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.
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_URLis 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.
/healthzis 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
- Revoke the exposed key in the console.
- Issue a replacement and update the consumer through its secret manager.
- Review
authorization_eventsfor the key ID and affected time window. - Investigate the exposure source; never restore a revoked key.