Skip to main content

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, webhook profile → NATS JetStream)
  • React webapp (served via nginx)
  • PostgreSQL 17 (custom image with pg_partman — see ADR 0018; vanilla postgres:17 is not a drop-in)
  • Hephaestus-native auth (Spring Security oauth2Login federating 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:

VariablePurpose
HEPHAESTUS_AUTH_ISSUERPublic 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_PROFILESelects 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_REDIRECTPost-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

VariablePurpose
HEPHAESTUS_INTEGRATION_SLACK_ENABLEDEnables the Slack OAuth admin surface (per-workspace connect). Off by default.
HEPHAESTUS_INTEGRATION_SLACK_CLIENT_IDSlack app client ID used by the OAuth flow
HEPHAESTUS_INTEGRATION_SLACK_CLIENT_SECRETSlack app client secret used by the OAuth flow
HEPHAESTUS_INTEGRATION_SLACK_REDIRECT_URIRequired 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_SECRETRequired 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/slack for workspace app installation.
    • <api-public-base-url>/login/oauth2/code/slack for 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:

ScopeFeature
chat:writeMentor DM replies and admin/leaderboard-channel announcements
assistant:writeThe DM "Thinking…" streaming status and suggested prompts on the Agent messaging surface
im:historyReceiving the DM message events that drive a mentor turn
channels:history / groups:historyReading messages in workspace-admin-consented monitored channels (public / private)
channels:read / groups:readChannel directory listing for the admin channel picker and periodic channel-metadata refresh
channels:joinLetting an admin add a public channel to monitoring directly from the picker
users:readResolving 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.

VariablePurpose
HEPHAESTUS_INTEGRATION_OUTLINE_ENABLEDEnables 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_URLOutline 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_IDOutline OAuth app client ID (redirect https://<host>/api/login/oauth2/code/outline)
OUTLINE_OAUTH_CLIENT_SECRETOutline OAuth app client secret
OUTLINE_OAUTH_DISPLAY_NAMELabel 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 501 to every delivery on POST /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 request openid. read is what POST /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:

VariablePurpose
GITLAB_OAUTH_CLIENT_IDGitLab OAuth application client ID (callback https://<host>/api/login/oauth2/code/gitlab)
GITLAB_OAUTH_CLIENT_SECRETGitLab OAuth application client secret
GITLAB_OAUTH_BASE_URLGitLab 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_NAMELogin-button label (defaults to GitLab; e.g. gitlab.lrz.de)
GITLAB_DEFAULT_SERVER_URLDefault GitLab instance for workspace creation / SCM sync (not auth). Falls back to GITLAB_OAUTH_BASE_URL
GITLAB_ENABLEDEnables server-side GitLab beans in the application server
GITLAB_WORKSPACE_CREATIONEnables GitLab workspace creation in the UI and API
WEBHOOK_SECRETShared secret used for GitLab webhook verification and auto-registration
WEBHOOK_EXTERNAL_URLPublic 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:

VariablePurpose
PRACTICE_REVIEW_FOR_ALLEnables the feature flag and the detection gate for all users. Also sets the detection gate via PRACTICE_REVIEW_RUN_FOR_ALL.
PRACTICE_REVIEW_SKIP_DRAFTSSkips draft PRs and draft merge requests
PRACTICE_REVIEW_DELIVER_TO_MERGEDAllows delivery after merge
PRACTICE_REVIEW_COOLDOWN_MINUTESMinimum delay between repeated reviews of the same PR/MR
AGENT_ENABLEDEnables the PostgreSQL-backed agent job queue and executor
GIT_CHECKOUT_ENABLEDEnables local repo checkout and bind-mount into agent containers
NATS_ENABLEDEnables webhook-driven sync consumption (unrelated to the agent job queue)
NATS_DURABLE_CONSUMER_NAMEDurable 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; 0 when 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 by jobType and outcome status (COMPLETED, FAILED, TIMED_OUT, CANCELLED, REQUEUED).
  • agent.job.delivery.recovered — stuck PENDING deliveries (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:

EnvironmentIntended scope
PreviewLimited by default. Keep GitLab login, GitLab workspaces, sandbox, git checkout, and agent job execution disabled unless preview is explicitly being used as rollout validation.
StagingUses 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.
ProductionFull 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:

VariableDefaultWhat 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_LOOPBACKfalseWhether 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_URLECB daily reference feedOverride 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.

  1. 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.
  2. 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.
  3. 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.held counts jobs parked on a cap; agent.queue.depth counts only what a worker could claim now, so alert on held rather than reading depth.
  4. 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.
  5. Is AGENT_ENABLED=true on every role that needs it? It defaults to false and 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_seconds and agent.queue.running are absent from that pod's metrics rather than reading zero.
  6. 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 — check AGENT_ENABLED and the worker role on the worker pod. Reviews also need GIT_CHECKOUT_ENABLED; it and AGENT_ENABLED must 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.

VariableDefaultPurpose
MONITORING_RUN_ON_STARTUPtrueRun initial sync when application starts
MONITORING_TIMEFRAME7Days of recent activity to sync each cycle
MONITORING_SYNC_CRON0 0 * * * *Cron schedule for sync (hourly by default)
MONITORING_SYNC_COOLDOWN_IN_MINUTES60Minimum gap between syncs per repository
MONITORING_BACKFILL_ENABLEDtrueEnable historical data backfill
MONITORING_BACKFILL_BATCH_SIZE50Issues/PRs per backfill batch
MONITORING_BACKFILL_RATE_LIMIT_THRESHOLD500Skip backfill if rate limit below this
MONITORING_BACKFILL_INTERVAL_SECONDS60Interval 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.

  1. Provision infrastructure: Ensure PostgreSQL, NATS, and storage volumes are ready. (The self-host stack brings its own; this one does not.)
  2. Configure the environment: Copy docker/.env.example to docker/.env and fill it in per Install § 2 plus the three reference-only variables above. Note server/.env is the local-development file for mvn spring-boot:run — no Compose service reads it.
  3. Bootstrap secrets: Load those variables into your secret manager or the .env files consumed by Docker/Kubernetes.
  4. Deploy services: Use docker/compose.proxy.yaml, docker/compose.core.yaml and docker/compose.app.yaml, or your Kubernetes manifests.
  5. Run database migrations: The application server runs Liquibase migrations on startup; monitor logs to confirm success.
  6. 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_ENABLED and NATS_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.
    • /imprint and /privacy do 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.