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

80 lines
4 KiB
Markdown

# 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](installation.md).
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:
```text
https://<go-loose-host>/auth/callback
```
The web installer is preferred. Environment configuration remains available for existing or fully automated deployments:
```dotenv
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:
```text
0002_membership_invitations.sql
```
Use this structure:
```sql
-- +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.