go-loose/docs/operations.md
Daniel Noulet 8d8970d5a7 test: drive the whole application over HTTP, and serialise migrations
The behaviour suite in internal/behaviour installs the application into an
empty database through the web installer, signs in, walks every flow, and then
replaces the process to confirm the state was persisted rather than held in
memory. It found four defects, each fixed here or in the preceding commit:

- Migrate was not safe to run from two instances at once. CREATE TABLE IF NOT
  EXISTS is not race free in PostgreSQL and collided on the pg_type unique
  index, and two replicas could apply the same file. The run now holds a
  session-level advisory lock, so a rolling deployment waits instead of failing.

- Bootstrap refused ownership whenever a tenant held any member, so installing
  with the demonstration seed and then setting a local administrator password
  produced an account that could never reach the console. It now grants
  ownership only while the tenant has no owner, which still never takes a tenant
  over from one that does.

Three of the fixes are covered by store-level regression tests in the same
commit: the ownership rule, the duplicate tenant slug conflict, and concurrent
migration runs.

The suite is skipped by go test ./... because it needs a scratch database and
the Google credentials for the installer. Run it with make behaviour-test, which
also resets the database afterwards. make test no longer inherits the database
variables, so unit runs need nothing.

Completes the earlier correction to the docs: the role matrix lives in the
database rather than in internal/role, and a local administrator claims the
system administrator role when no Google account has.
2026-10-05 19:37:09 +02:00

4 KiB

Operations

First-run installer

New deployments need only PostgreSQL, the public URL settings, and a stable session secret. Open /install to configure Google SSO and optional Guess demo data. See First-run installation and demo data.

The first Google SSO user is claimed as system administrator. All later SSO users remain in the waiting room until that administrator grants tenant membership and application access.

OIDC / SSO

Register an OIDC confidential web client with this callback:

https://<go-loose-host>/auth/callback

The web installer is preferred. Environment configuration remains available for existing or fully automated deployments:

GO_LOOSE_BASE_URL=https://auth.example.com
GO_LOOSE_AUTH_DOMAIN=auth.example.com
GO_LOOSE_OIDC_ISSUER=https://id.example.com/realms/platform
GO_LOOSE_OIDC_CLIENT_ID=go-loose
GO_LOOSE_OIDC_CLIENT_SECRET=<from-secret-manager>
GO_LOOSE_OIDC_REDIRECT_URL=https://auth.example.com/auth/callback
GO_LOOSE_ALLOWED_EMAIL_DOMAINS=example.com
GO_LOOSE_DEV_LOGIN=false

System-administrator SSO is served on https://auth.example.com. Client applications and tenant password login use https://<tenant>.auth.example.com; configure wildcard DNS and TLS for *.auth.example.com.

Database authentication is controlled by GO_LOOSE_LOCAL_LOGIN. It may remain enabled for seeded or invited users without creating a bootstrap account. Set GO_LOOSE_LOCAL_ADMIN_PASSWORD only when compatibility bootstrap behavior is explicitly required, and provide it through a secret manager.

The provider must return sub, email, and preferably name claims.

Migrations

Migration files live in internal/database/migrations and are embedded into the binary. Files run lexicographically once, in individual transactions.

A migration run holds a session-level advisory lock, so several replicas may start at the same time against one database. Without it the run races with itself: PostgreSQL's CREATE TABLE IF NOT EXISTS is not race free and can fail with a duplicate pg_type row, and two replicas can try to apply the same file. The second replica waits and then finds the work already recorded.

Create upgrades with an immutable numbered file:

0002_membership_invitations.sql

Use this structure:

-- +goose Up
CREATE TABLE membership_invitations (...);

-- +goose Down
DROP TABLE membership_invitations;

The built-in runner applies only the Up section. Down sections document rollback intent but are never run automatically. Never edit an applied migration; create a new one. Prefer backward-compatible expansion first, deploy code that understands both shapes, then remove old fields in a later release.

Back up the database before production upgrades. Restore and test backups regularly. Startup fails if a migration fails, preventing the application from running against a partially upgraded schema.

Production checklist

  • TLS terminates at a trusted ingress and forwards the original host.
  • GO_LOOSE_BASE_URL is the exact HTTPS browser origin.
  • Session and OIDC secrets come from a secret manager.
  • Demo users are not seeded, or every demo credential is removed/rotated before exposure.
  • Development login and unrestricted private URL scanning are disabled.
  • PostgreSQL uses TLS, backups, point-in-time recovery, and least-privilege credentials.
  • /healthz is used for readiness; logs are shipped without request headers.
  • Authorization endpoint rate limits and network policies are configured at ingress.
  • Multiple replicas share PostgreSQL; rolling deployment starts only after migration compatibility is verified.
  • The external Swagger UI assets are mirrored locally if internet-free operation is required.

Key incident response

  1. Revoke the exposed key in the console.
  2. Issue a replacement and update the consumer through its secret manager.
  3. Review authorization_events for the key ID and affected time window.
  4. Investigate the exposure source; never restore a revoked key.