Integrations & Reference Deployment
:::info Which page owns what
Install (Self-Hosted) owns standing up an instance — requirements, the base .env,
the GitHub OAuth app, the first admin, first boot, upgrades, and the install troubleshooting table.
It is the only supported install path.
This page owns two things and nothing else: the per-integration variable reference (GitLab,
Slack, Outline, practice review, sync) and the reference TUM deployment — the multi-file compose
stack in docker/compose.{proxy,core,app}.yaml, which self-hosters do not use.
Anything documented on both pages was documented wrong. Where a fact belongs to the install, this page links rather than restates it. :::
This page assumes familiarity with Docker Compose. The TUM sections additionally assume access to the protected secrets managed by TUM.
Platform overview
The reference stack (not the self-host one — that stack is listed on the install page) consists of:
- Application server (Spring Boot, runs the Pi mentor agent in-process)
- Webhook server (same Spring Boot artifact,
webhookprofile → NATS JetStream) - React webapp (served via nginx)
- PostgreSQL 17 (custom image with
pg_partman— see ADR 0018; vanillapostgres:17is not a drop-in) - Hephaestus-native auth (Spring Security
oauth2Loginfederating to GitHub + optional GitLab — gitlab.com or self-hosted — issuing cookie-session JWTs — ADR 0017)
Base environment variables
The required secrets — HEPHAESTUS_SECURITY_ENCRYPTION_KEY, HEPHAESTUS_AUTH_STATE_COOKIE_KEY,
WEBHOOK_SECRET, POSTGRES_PASSWORD, the GitHub OAuth pair, and
HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS — are documented once, on the
Install guide, along with how to generate each and the warning about
never changing the encryption key. Everything there applies to this stack too.
Three variables exist only in the reference deployment, so they live here:
| Variable | Purpose |
|---|---|
HEPHAESTUS_AUTH_ISSUER | Public issuer origin, e.g. https://<host> (sets the JWT iss + /.well-known issuer). The self-host compose file derives this from APP_HOSTNAME; the multi-file stack does not. |
LEGAL_PROFILE | Selects a bundled imprint/privacy profile. Use tumaet only for the canonical AET deployment; self-hosters leave empty and mount /legal-overrides/. See Legal Pages. |
HEPHAESTUS_INTEGRATION_OAUTH_SUCCESS_REDIRECT / _FAILURE_REDIRECT | Post-callback landing URLs for integration OAuth. Success defaults to /integrations?status=success, failure to the success base + ?status=error. Set absolute URLs when the SPA host differs from the API host. |
:::warning GH_, not GITHUB_
In docker/.env the GitHub OAuth keys are GH_OAUTH_CLIENT_ID / GH_OAUTH_CLIENT_SECRET: GitHub
Actions reserves the GITHUB_ prefix, so Compose maps them to the server's
GITHUB_OAUTH_CLIENT_ID / _SECRET. Setting GITHUB_OAUTH_CLIENT_ID directly in docker/.env has
no effect, and the provider is skipped with no sign-in button to show for it.
:::
HEPHAESTUS_SECURITY_ENCRYPTION_KEY carries one extra constraint on an instance that predates the
unified integration framework: it must be set before that first deploy, because the Liquibase
backfill re-encrypts existing credentials on boot and fails fast — or HALTs the migration — without
it.
Slack app configuration
| Variable | Purpose |
|---|---|
HEPHAESTUS_INTEGRATION_SLACK_ENABLED | Enables the Slack OAuth admin surface (per-workspace connect). Off by default. |
HEPHAESTUS_INTEGRATION_SLACK_CLIENT_ID | Slack app client ID used by the OAuth flow |
HEPHAESTUS_INTEGRATION_SLACK_CLIENT_SECRET | Slack app client secret used by the OAuth flow |
HEPHAESTUS_INTEGRATION_SLACK_REDIRECT_URI | Required when Slack is enabled and the app has multiple Slack redirect URLs. Use the public application-server callback URL, including any proxy path prefix — e.g. https://<host>/api/oauth/callback/slack. |
HEPHAESTUS_INTEGRATION_SLACK_SIGNING_SECRET | Required when Slack is enabled: verifies Slack Events API and interactivity POSTs on the webhook server |
Per-workspace bot tokens are issued via OAuth and encrypted at rest in the connection table; there
is no global bot token.
When Slack is enabled, configure one Slack app. Use the public application-server base URL including any proxy path
prefix for OAuth URLs. In the production compose stack that base is https://<host>/api.
- OAuth redirect URLs (include any API path prefix your proxy uses):
<api-public-base-url>/oauth/callback/slackfor workspace app installation.<api-public-base-url>/login/oauth2/code/slackfor member account linking.
- Events request URL:
https://<host>/webhooks/slack - Interactivity request URL:
https://<host>/webhooks/slack/interactivity - App Home: enabled. Slack opens the Home tab when someone clicks the app; that is expected. Hephaestus uses Home for account linking and privacy controls, while actual mentoring happens in the Messages tab / DM.
If you run the webhook receiver on a separate public host, route only /webhooks/slack and
/webhooks/slack/interactivity to the webhook role. The OAuth and account-linking paths
(/oauth/callback/slack, /auth/login, /oauth2/authorization/*, /login/oauth2/code/slack, with the same
public prefix if you use one) must reach the application-server role.
The bot token scope set (SlackConnectionStrategy.DEFAULT_SCOPES) stays minimal — every scope maps to a
feature the code exercises:
| Scope | Feature |
|---|---|
chat:write | Mentor DM replies and admin/leaderboard-channel announcements |
assistant:write | The DM "Thinking…" streaming status and suggested prompts on the Agent messaging surface |
im:history | Receiving the DM message events that drive a mentor turn |
channels:history / groups:history | Reading messages in workspace-admin-consented monitored channels (public / private) |
channels:read / groups:read | Channel directory listing for the admin channel picker and periodic channel-metadata refresh |
channels:join | Letting an admin add a public channel to monitoring directly from the picker |
users:read | Resolving a Slack user to a workspace member for mentor identity and digest delivery |
The user token scope set is only for Sign in with Slack account linking:
openid
profile
email
Also create an enabled Hephaestus login provider with registration id slack, type SLACK,
base URL https://slack.com, and scopes openid profile email. Workspace app installation
alone gives Hephaestus a bot token; personalized mentor DMs also need the member to link
their Slack identity to their Hephaestus account.
Do not add unused broad scopes such as chat:write.public, commands, team:read,
app_mentions:read, im:write, mpim:read, or users:read.email unless code is added that
actually uses them.
Use slack-app-manifest-template.yml as the dashboard baseline —
it is the single source of truth for scopes, event subscriptions, and request URLs. Its
settings.event_subscriptions list covers the DM/channel message events that drive mentor turns and
channel ingestion, the channel-lifecycle events that keep the monitored-channel allow-list in sync, and
the app-lifecycle events.
The nightly Slack history-reconciliation sweep replays conversations.history for already-consented
channels through this same bot token, so it needs no scopes beyond the set above.
Outline integration
Outline is a documentation content source: a workspace admin connects one Outline instance from
Settings → Integrations with the wiki's base URL and an API key, then registers the collections to
mirror. The key is encrypted at rest in the connection table.
| Variable | Purpose |
|---|---|
HEPHAESTUS_INTEGRATION_OUTLINE_ENABLED | Enables the Outline beans (connect, sync, webhook, content projection). Every Outline bean is gated on it and defaults to off, so an unset flag means the integration is entirely absent — no admin surface, no sync, no webhook verification. |
OUTLINE_OAUTH_BASE_URL | Outline instance the account-linking flow federates to, e.g. https://wiki.example.com. Required whenever OUTLINE_OAUTH_CLIENT_ID is set — Outline is self-hosted, so there is no default origin. Blank or invalid, the provider seed is skipped and the reason is logged at ERROR naming the offending variable (the app still boots; the linking button is simply never there). |
OUTLINE_OAUTH_CLIENT_ID | Outline OAuth app client ID (redirect https://<host>/api/login/oauth2/code/outline) |
OUTLINE_OAUTH_CLIENT_SECRET | Outline OAuth app client secret |
OUTLINE_OAUTH_DISPLAY_NAME | Label shown on the account-linking button (defaults to Outline) |
HEPHAESTUS_INTEGRATION_OUTLINE_ENABLED must be set on all three roles, and the compose files pass
it to each with the same value:
- application-server — connect/admin API, collection registration, the reconcile scheduler, and the webhook-subscription registrar.
- application-worker — the agent context is assembled on the worker, and the Outline content source carries no runtime-role gate. Without the flag there, reviews and mentor turns run with a context that silently contains no wiki documents.
- webhook-server — without the flag no Outline signature verifier is registered, so the receiver
answers
501to every delivery onPOST /webhooks/outline. Outline counts those as failures and auto-disables the subscription after 25 of them.
There is no Outline signing-secret environment variable. Unlike Slack's one global signing secret,
Outline's webhook secret is minted per subscription when Hephaestus registers it and stored encrypted on
the connection row; the receiver reads it back through JPA. The subscription is registered automatically
when a workspace connection is activated — its delivery URL is https://<host>/webhooks/outline, derived
from WEBHOOK_EXTERNAL_URL (defaulting to the application host).
The OUTLINE_OAUTH_* block is independent of the workspace connection: it seeds a link-only login
provider (registration id outline) that lets a member attach their Outline identity to their
Hephaestus account so documents can be attributed to them. There is no "sign in with Outline" — the
provider never appears on the login picker. A blank OUTLINE_OAUTH_CLIENT_ID leaves it unseeded, and
the provider can also be added later under Instance admin → Login providers.
Registering the OAuth app in Outline (Settings → Applications, admin-only):
- Redirect URI:
https://<host>/api/login/oauth2/code/outline— Outline matches redirect URIs by exact string, so it must include the API path prefix your proxy uses and carry no trailing slash. Between 1 and 10 URIs are allowed; all must be HTTPS except loopback. - Scope:
read. Outline is plain OAuth2, not OIDC — never requestopenid.readis whatPOST /api/auth.info(the identity probe) needs. - PKCE: S256, sent automatically. Nothing to configure.
- Access tokens live one hour; refresh tokens live 30 days and rotate on use with reuse detection.
API keys do not self-rotate. Outline's apiKeys.create and apiKeys.delete accept only an
interactive session, never a bearer token, so Hephaestus cannot roll its own key. The connection's
admin card surfaces the key's expiry (via apiKeys.list, when the key may see itself) ahead of the
lapse; renewing means creating a new key in Outline and re-entering it in Settings → Integrations.
A lapsed key reports accepted=false and the mirror stops refreshing until it is replaced.
GitHub integration
Hephaestus can connect to GitHub using either a Personal Access Token (PAT) or a GitHub App. The full setup — mode comparison, required GitHub App permissions, the webhook event list, and manual webhook creation (GitHub has no auto-registration, unlike GitLab) — lives in GitHub Integration.
Environment variables for GitHub App mode: GH_APP_ID, GH_APP_PRIVATE_KEY (or GH_APP_PRIVATE_KEY_LOCATION), GH_APP_INSTALLATION_URL. If not provided (GH_APP_ID=0, the default), workspaces use PATs configured through the UI.
GitLab rollout bundle
GitLab login, GitLab workspaces, webhook auto-registration, and practice review are not independent toggles. Treat the following as a rollout bundle and enable them together when rolling out GitLab practice review:
| Variable | Purpose |
|---|---|
GITLAB_OAUTH_CLIENT_ID | GitLab OAuth application client ID (callback https://<host>/api/login/oauth2/code/gitlab) |
GITLAB_OAUTH_CLIENT_SECRET | GitLab OAuth application client secret |
GITLAB_OAUTH_BASE_URL | GitLab instance the login button federates to. Defaults to https://gitlab.com; set to your self-hosted instance (e.g. https://gitlab.lrz.de) |
GITLAB_OAUTH_DISPLAY_NAME | Login-button label (defaults to GitLab; e.g. gitlab.lrz.de) |
GITLAB_DEFAULT_SERVER_URL | Default GitLab instance for workspace creation / SCM sync (not auth). Falls back to GITLAB_OAUTH_BASE_URL |
GITLAB_ENABLED | Enables server-side GitLab beans in the application server |
GITLAB_WORKSPACE_CREATION | Enables GitLab workspace creation in the UI and API |
WEBHOOK_SECRET | Shared secret used for GitLab webhook verification and auto-registration |
WEBHOOK_EXTERNAL_URL | Public webhook base URL registered on GitLab |
The login provider is instance-agnostic: gitlab.com works out of the box, and any self-hosted GitLab works by pointing GITLAB_OAUTH_BASE_URL at it. Create the GitLab OAuth application on that instance with these settings:
- Redirect URI:
https://<hostname>/api/login/oauth2/code/gitlab· Scope:read_user - Confidential client: enabled
This env-seeds one default GitLab login provider (registration id gitlab). Additional GitLab instances are added at runtime by an instance admin under Instance admin → Login providers — no redeploy.
Practice review rollout bundle
Practice review is not enabled by a single flag. The following settings must be aligned:
| Variable | Purpose |
|---|---|
PRACTICE_REVIEW_FOR_ALL | Enables the feature flag and the detection gate for all users. Also sets the detection gate via PRACTICE_REVIEW_RUN_FOR_ALL. |
PRACTICE_REVIEW_SKIP_DRAFTS | Skips draft PRs and draft merge requests |
PRACTICE_REVIEW_DELIVER_TO_MERGED | Allows delivery after merge |
PRACTICE_REVIEW_COOLDOWN_MINUTES | Minimum delay between repeated reviews of the same PR/MR |
AGENT_ENABLED | Enables the PostgreSQL-backed agent job queue and executor |
GIT_CHECKOUT_ENABLED | Enables local repo checkout and bind-mount into agent containers |
NATS_ENABLED | Enables webhook-driven sync consumption (unrelated to the agent job queue) |
NATS_DURABLE_CONSUMER_NAME | Durable consumer name for sync processing |
The Docker sandbox itself is activated by the worker runtime role (hephaestus.runtime.worker.enabled), not by AGENT_ENABLED. Keep that capability and Docker access on the application server because interactive mentor sandboxes are request-affine and run there; a dedicated application-worker adds capacity for queued practice reviews. When AGENT_ENABLED is also true, any worker-capable replica may claim those PostgreSQL jobs. AGENT_ENABLED and GIT_CHECKOUT_ENABLED must be true together for practice review — anything else is a half-configured deployment. NATS_ENABLED still gates webhook-driven sync consumption and must stay on for that, but it is no longer part of the practice-review bundle.
Agent queue health & retention
The agent job queue (agent_job, polled by AgentJobExecutor) exposes its own health signals — wire an alert off these rather than guessing from application logs:
agent.queue.depth— QUEUED jobs currently eligible to run (available_at <= now()).agent.queue.oldest_age_seconds— age of the oldest eligible QUEUED job;0when the queue is empty. This is the signal to alert on, not depth alone: a briefly busy queue and a stuck one can both show non-zero depth, but only a stuck queue shows a climbing age.agent.queue.running— jobs currently RUNNING fleet-wide.agent.queue.health.sampler.failures— failed queue-health samples; gauges keep their last-good values instead of reporting a false empty queue.agent.job.claim.latency— time between a job becoming eligible and being claimed.agent.job.execution.duration— tagged byjobTypeand outcomestatus(COMPLETED,FAILED,TIMED_OUT,CANCELLED,REQUEUED).agent.job.delivery.recovered— stuckPENDINGdeliveries (executor crashed mid-delivery) successfully re-attempted by the recovery sweep.agent.job.retention.stripped/agent.job.retention.deleted— terminal rows whose heavy payload columns were nulled out, and rows deleted outright, by the retention sweep below.
The queue-health gauges are sampled every 30s; timers and counters are recorded inline. None run on a request path.
Retention: terminal agent_job rows are pruned automatically so the table doesn't grow unbounded on a busy instance. AGENT_PAYLOAD_RETENTION (default P14D) strips container_logs/output to NULL past that age; AGENT_ROW_RETENTION (default P90D, must be >= AGENT_PAYLOAD_RETENTION) deletes the row outright past that age. Both accept an ISO-8601 duration. The defaults are deliberately generous — tighten them only if storage pressure demands it, since a stripped-but-present row is still useful for a "how many jobs ran" count even after its diagnostic payload is gone.
Rollout tiers
Use the same variables differently across environments:
| Environment | Intended scope |
|---|---|
| Preview | Limited by default. Keep GitLab login, GitLab workspaces, sandbox, git checkout, and agent job execution disabled unless preview is explicitly being used as rollout validation. |
| Staging | Uses the production compose files, but should still be a controlled rollout. Start by enabling GitLab login and GitLab workspace creation first. Only enable AGENT_ENABLED and GIT_CHECKOUT_ENABLED when staging is intentionally validating practice review execution. |
| Production | Full rollout only after staging has validated the exact same bundle. |
Staging and production both use the production compose files. The difference should come from the .env values, not from a different compose topology.
Instance-wide LLM settings
The mentor's interactive sandbox uses the application-server replica's LLM proxy; a queued practice review uses the proxy beside the worker that claimed it. That proxy is the only path a sandbox has to a provider key, which is never injected directly. There is no separate intelligence service to configure.
No provider, model, price, or credential is ever set through the environment. All of that is runtime catalog data entered in the admin UI, and it is documented in full — field by field, with what the connection test does and a worked pricing example — on Connect an AI Provider.
Three instance-wide LLM knobs are env vars, because they are deploy-time facts rather than catalog data. All three are optional and every one of them defaults to off or to the standard value:
| Variable | Default | What it does |
|---|---|---|
HEPHAESTUS_LLM_DISPLAY_CURRENCY | (empty) | Currency to show spend in alongside USD. Empty turns the second currency off entirely. EUR is the only supported value, and anything else — including a perfectly well-formed code such as GBP — fails startup naming what is accepted, so a currency this instance cannot convert to can never hide as a missing feature. |
HEPHAESTUS_LLM_EGRESS_ALLOW_LOOPBACK | false | Whether http://localhost provider base URLs may be saved and dialled. Never set this in production — it is an SSRF hole that lets a workspace admin aim a "provider" at host-local services. The private/link-local/CGNAT check is unconditional and this flag does not relax it. |
HEPHAESTUS_LLM_FX_DAILY_URL | ECB daily reference feed | Override only on an air-gapped instance mirroring the ECB file internally. |
The proxy resolves the upstream base URL, auth header, and key per model from whichever connection it is bound to, so nothing needs to be pinned at the process level. On a split-pod deployment the worker pod resolves providers from the same catalog as the app pod — no LLM credential passthrough between pods. Warm mentor sandboxes are reused only while their complete non-secret runtime identity still matches; changing the bound model/route, image, prompt/runtime files, or limits replaces the sandbox before the next prompt. Per-turn context is refreshed inside a compatible warm sandbox rather than forcing a restart.
Workspace budgets and alerting
There are two monthly LLM budgets, and they are never added together:
- The instance budget caps a workspace's spend on shared models — your money, on connections you registered. An instance admin sets it under Instance admin → AI usage → Set budget (per-workspace, per calendar month, UTC).
- The workspace's own budget caps that workspace's spend on its own connected provider — their money, billed to their provider account. A workspace admin sets it on their own AI usage page.
For either cap: clearing the field removes it (No cap — uncapped, the default); setting it to exactly 0 pauses that purse immediately, which is the supported way to hard-stop spending mid-month.
Each cap pauses only the work it funds. Exhausting the instance budget stops shared-model work and leaves the workspace's own-provider work running; exhausting the workspace's own budget does the reverse. This is deliberate — an instance budget that also paused work you are not billed for would be stopping spending you cannot see the price of. If you need to stop all AI work in a workspace, change the workspace's status or its feature switches; a budget is not that instrument.
Once a month's confirmed spend reaches a cap, that purse's budget verdict flips to EXHAUSTED. New agent jobs funded from it are refused at submission, queued jobs are checked again immediately before claim, and mentor turns are refused before starting a sandbox. Work resumes automatically at the next UTC calendar month, or within about a minute of the cap being raised or cleared. A workspace admin sees the pause explained on their workspace's AI usage page and, so it isn't only visible on that one screen, on the AI models page too.
The two surfaces fail differently, and you should staff for the difference. Practice detection goes silent — a queued job is refused before it runs and nothing is posted, so no end user sees an error. The mentor does not go silent: a user who sends a message into an exhausted purse gets a reply in the chat saying the monthly cap is reached and naming who can lift it (a workspace admin for the workspace's own-provider cap, an instance admin for the shared budget). That is deliberate — silently dropping a turn someone is waiting on reads as a broken product — but it does mean users will see and report a cap being hit, so expect the support contact rather than only a dashboard change.
Some started attempts may not return trustworthy usage counters, and migrated legacy attempts may not have an admission price snapshot. Such events are recorded but carry no price, and are excluded from the monthly totals — so the reported total is a floor, not the confirmed spend. The UI does not fold them into the number; it shows the confirmed total alongside a separate count of unpriced calls, and the purse's verdict reads UNVERIFIABLE instead of WITHIN (EXHAUSTED still takes priority). A non-zero unpriced count is your signal that real spend may be higher than displayed. New models cannot be enabled until an administrator declares either a price or No metered API cost; the latter counts as confirmed $0 spend, not unpriced.
There is deliberately no setting for what an UNVERIFIABLE month does, because the answer follows from the cap itself. An uncapped purse is never paused by unpriced usage — with no cap to enforce, there is nothing the missing price could breach. A capped purse whose month is UNVERIFIABLE is paused exactly like an exhausted one, because a cap you cannot verify is not a cap. It clears when the usage is reconciled or the next UTC month begins.
Rather than a policy knob, the guard is structural: a model cannot be enabled at all until someone declares its price or marks it No metered API cost, so a correctly administered instance does not reach an UNVERIFIABLE month in the first place.
Alerting hook: there is no built-in push notification when a workspace crosses its cap — wire your own alert off the Micrometer counters the budget service already emits: llm.budget.exhausted (incremented once per usage event whose confirmed contribution pushes a workspace's month-to-date spend across the instance cap — the purse you fund, so the one you are alerted on) and llm.budget.blocked (tagged surface by admission point, including agent_job and mentor; the agent-job tag also carries cap, naming which purse refused the work). Each EXHAUSTED transition also logs a WARN, visible in Sentry if configured.
Reasoning tokens are retained as telemetry when the compatible endpoint reports them. They are not priced separately: OpenAI-compatible usage includes reasoning tokens in billable output tokens, so a second reasoning rate would double-count the same work.
A workspace stopped running practice reviews
Nothing errors when practice review stops — the workspace simply goes quiet, so this is the one failure you have to go looking for. Work down the list; the first four are per-workspace and the last two are instance-wide, so if several workspaces went quiet at once, start at the bottom.
- Is a detection model bound and enabled? Workspace → Administration → AI models, the
"Detection runs on …" card. A workspace with no binding runs no reviews at all — this is the
expected state immediately after upgrading from named agent configurations, where everything is
carried over switched off (see
MIGRATION.md). The card's readiness indicator also covers the next case. - Is the bound model still usable? A model is only usable if the model and its connection are both enabled, the protocol is supported, and — for a shared instance model — the workspace still has a grant. Revoking a grant or disabling a connection stops every workspace bound to it, and the binding stays in place looking correct.
- Is a budget exhausted? Administration → AI usage names which purse paused the workspace
and who can lift it. Jobs already queued are held, not cancelled, and released automatically
when the cap is raised or the month rolls over — but a job still over cap seven days after it was
queued is cancelled rather than held forever.
agent.queue.heldcounts jobs parked on a cap;agent.queue.depthcounts only what a worker could claim now, so alert on held rather than reading depth. - Is the feature or the workspace switched off? Practice review is a workspace feature flag, and a paused workspace runs nothing regardless of its AI configuration.
- Is
AGENT_ENABLED=trueon every role that needs it? It defaults tofalseand gates submission, execution and orphan recovery independently of the worker role. Set on the worker only, jobs are submitted by nobody; set on the server only, they queue and are never claimed. Confirm by presence, not value: if the flag never reached a pod,agent.queue.depth,agent.queue.oldest_age_secondsandagent.queue.runningare absent from that pod's metrics rather than reading zero. - Are jobs being claimed at all? Watch
agent.queue.oldest_age_seconds. It should rise and fall. Climbing monotonically means the server is submitting and no worker is claiming — checkAGENT_ENABLEDand the worker role on the worker pod. Reviews also needGIT_CHECKOUT_ENABLED; it andAGENT_ENABLEDmust be true together.
The per-workspace job list under Administration → Practices → Runs shows each run's status, the model it ran on, and its error message — the fastest way to tell "never submitted" from "submitted and failed". A held job says it is held, why, and when it is next due; AI usage names which purse is capped.
Data sync and backfill
The sync scheduler fetches recent GitHub activity (issues, PRs, reviews) for monitored repositories. Backfill optionally syncs historical data in batches to avoid rate limit exhaustion.
| Variable | Default | Purpose |
|---|---|---|
MONITORING_RUN_ON_STARTUP | true | Run initial sync when application starts |
MONITORING_TIMEFRAME | 7 | Days of recent activity to sync each cycle |
MONITORING_SYNC_CRON | 0 0 * * * * | Cron schedule for sync (hourly by default) |
MONITORING_SYNC_COOLDOWN_IN_MINUTES | 60 | Minimum gap between syncs per repository |
MONITORING_BACKFILL_ENABLED | true | Enable historical data backfill |
MONITORING_BACKFILL_BATCH_SIZE | 50 | Issues/PRs per backfill batch |
MONITORING_BACKFILL_RATE_LIMIT_THRESHOLD | 500 | Skip backfill if rate limit below this |
MONITORING_BACKFILL_INTERVAL_SECONDS | 60 | Interval between backfill batches |
Backfill behavior: After recent sync completes, backfill works backwards from the highest issue number, syncing in small batches. It pauses when rate limits drop below the threshold and resumes on the next cycle. Progress is checkpointed per repository.
Disconnecting erases the mirror. Disconnecting a workspace's GitHub, GitLab, Slack, or Outline connection — and purging a workspace — hard-deletes the data mirrored through it, keeping only the operational sync history. It is irreversible and reconnecting performs a fresh initial sync. See the erasure triggers in the record of processing.
Deployment steps (reference stack)
The base install is the same one the Install guide walks through — the same secrets, the same GitHub OAuth app and callback, the same before-first-boot admin allowlist, the same legal-pages obligation. Only what is different about the multi-file stack is listed here.
- Provision infrastructure: Ensure PostgreSQL, NATS, and storage volumes are ready. (The self-host stack brings its own; this one does not.)
- Configure the environment: Copy
docker/.env.exampletodocker/.envand fill it in per Install § 2 plus the three reference-only variables above. Noteserver/.envis the local-development file formvn spring-boot:run— no Compose service reads it. - Bootstrap secrets: Load those variables into your secret manager or the
.envfiles consumed by Docker/Kubernetes. - Deploy services: Use
docker/compose.proxy.yaml,docker/compose.core.yamlanddocker/compose.app.yaml, or your Kubernetes manifests. - Run database migrations: The application server runs Liquibase migrations on startup; monitor logs to confirm success.
- Verify:
- The bootstrap admin can sign in and reach instance-admin navigation, and can reach workspace admin endpoints for workspaces they are a member of (auto-elevated to workspace ADMIN there).
- A test webhook from GitHub or GitLab reaches the ingest pipeline.
- Before testing GitLab practice review, confirm
GITLAB_ENABLED,GITLAB_WORKSPACE_CREATION,AGENT_ENABLED,GIT_CHECKOUT_ENABLEDandNATS_ENABLED(needed for sync/webhook ingest, not the agent job queue) are all present in the deployed environment — this is the bundle a preview environment most often lacks. /imprintand/privacydo not show the red "not configured" banner. See Legal Pages.
Global admin privileges
Accounts with app_role = 'APP_ADMIN' carry the app_admin authority in their JWT (admin is a reserved string that is actively stripped — it is the per-workspace role name, never an instance authority). This grants them:
- Automatic workspace ADMIN privileges for workspaces where they have membership (they are auto-elevated to workspace ADMIN level)
- Ability to manage workspace settings, members, and repositories in their member workspaces
- Cannot perform OWNER-only operations (e.g., workspace ownership transfer) unless explicitly granted the OWNER role in that workspace
- Must have workspace membership (any role: OWNER, ADMIN, or MEMBER) to access the workspace
This allows platform administrators to troubleshoot and manage workspaces where they are members, with automatic admin privileges, without needing explicit ADMIN role assignment in the database.
Operational tips
- Monitor services with the central Prometheus/Loki stack; ensure trace IDs appear in logs.
- Schedule regular backups for PostgreSQL (it now holds auth state — accounts, identity links, sessions — in addition to application data).
- Review weekly leaderboard Slack posts to ensure the automation is active.
- Preview deployments do not currently represent the full GitLab practice-review rollout unless they are explicitly wired with the same GitLab IdP, sandbox, git checkout, and agent job queue settings.
Support
Contact the Hephaestus core team if you need to rotate secrets or migrate infrastructure. Document any deviations from this checklist in the deployment runbook.