Skip to main content

Tenant lifecycle command

Read this when you change tenant provisioning, teardown or worker overrides, or run tadoku-tenant yourself.

services/tadoku-api/cmd/tadoku-tenant provides provisioning, teardown and worker overrides and email access tokens. Lifecycle operations take a full parsed test tenant key, such as tadoku/branch-0123abcd; the id must end in a hyphen and eight lowercase hexadecimal digits. Bare route ids, tadoku/prod and production registry rows are refused before provider writes.

bazel run //services/tadoku-api/cmd/tadoku-tenant -- provision \
--tenant tadoku/branch-0123abcd --flipt-features k8s/dev/base/flipt/features.yaml
bazel run //services/tadoku-api/cmd/tadoku-tenant -- override set \
--tenant tadoku/branch-0123abcd --component tadoku-worker
bazel run //services/tadoku-api/cmd/tadoku-tenant -- override clear \
--tenant tadoku/branch-0123abcd --component tadoku-worker
bazel run //services/tadoku-api/cmd/tadoku-tenant -- teardown \
--tenant tadoku/branch-0123abcd

Provision, teardown and override commands refuse credentials that neither own the tenant registry nor belong to the tadoku_tenant_lifecycle role. That role needs only schema usage, select, insert and delete on tenants and tenant_overrides, and execute on copy_production_platform_scoring(text) and copy_production_published_pages(text); registry policies limit its writes to test rows, and deleting a row cascades through the owner's foreign keys. Production branch Jobs use it, and development uses the owner. Credentials come from TENANT_POSTGRES_HOST, TENANT_POSTGRES_PORT (default 5432), TENANT_POSTGRES_DATABASE, TENANT_POSTGRES_USER, TENANT_POSTGRES_PASSWORD and TENANT_POSTGRES_SSLMODE. TENANT_POSTGRES_URL is rejected. Provision and teardown additionally require TENANT_VALKEY_URL and TENANT_FLIPT_MANAGEMENT_URL; TENANT_FLIPT_ENVIRONMENT defaults to test and must differ from production. The Flipt management URL must be absolute HTTP(S) without credentials, query or fragment; trailing slashes are removed. Use the direct provisioning-authorized Flipt endpoint, rather than the API's restricted service-token route. Override commands need only PostgreSQL. The only registered override component is jobqueue.WorkerComponent, currently tadoku-worker.

//services/tadoku-api/cmd/tadoku-tenant:image packages the command for branch provisioning outside development: a distroless base with the CA bundle verify-full connections need, /tadoku-tenant as its entrypoint and the repository's flag definitions at /flipt/features.yaml. Its branch_push target sets no repository or tags; the caller supplies both. The development //.dev:tenant_push image has no CA bundle.

Provision commits an idempotent test registry row under a transaction-local advisory lock. In the same transaction it copies tadoku/prod's active platform scoring rule set and its rules into the tenant, with new ids and a platform_scoring_config row pointing at the copy, so logs can be scored. It then copies tadoku/prod's published, undeleted CMS pages, each with only the content revision it displays, keeping namespaces, slugs, titles and timestamps under new ids. Drafts, scheduled and deleted pages, earlier revisions and posts are not copied. Each copy is skipped when the tenant already has platform scoring config or any page, so reruns keep the tenant's own changes; contest rule sets and user data are never copied. It grants no permissions. It then creates only missing Flipt resources from the strict boolean seed format. Canonical tadoku/prod, imports and the safe false default remain unchanged. Reruns preserve existing flag values. Teardown deletes the tenant's cache keys and Flipt namespace before deleting the registry row and its cascading data, including permission relationships. Failure leaves the row for a retry; cleanup still runs when a prior attempt already removed that row. Commands sharing a tenant must be serialized by the caller's route lease. Provider HTTP calls have a 30-second timeout; the command has a five-minute deadline and observes shutdown signals. Secrets are supplied through environment variables and never printed.

Development accounts and cleanup inspection​

The development hook invokes development-accounts before fixture seeding:

TENANT_PRODUCTION=false tadoku-tenant development-accounts \
--tenant tadoku/branch-0123abcd --admin-id <base-admin-uuid> --reader-id <base-reader-uuid>
tadoku-tenant inspect --tenant tadoku/branch-0123abcd

development-accounts requires the development database host tadoku-dev-db.tdk-dev-data, or loopback for local disposable tests, explicit TENANT_PRODUCTION=false, a registered test tenant and registry-management privileges. It uses the development fixture emails/passwords from TADOKU_DEV_ADMIN_EMAIL, TADOKU_DEV_ADMIN_PASSWORD, TADOKU_DEV_READER_EMAIL and TADOKU_DEV_READER_PASSWORD; defaults are dev@tadoku.app, reader@tadoku.app and the documented development password. It fixes display names to Dev Admin and Dev Reader. The owner supplies their existing dev base UUIDs; public registration never accepts fixture UUIDs.

The command hashes passwords at bcrypt cost 12 in the identity feature. Its fixture-only path permits the legacy short development password. Both inserts are transactional. Fixture lookups and writes name the tenant explicitly because the development owner bypasses row-level security. Existing fixture UUIDs keep their branch-owned credentials, email, profile, state, timestamps and sessions; only missing accounts are initialized. A configured email held by another UUID for a missing fixture fails the entire pair. Account inserts handle conflicts only on (tenant, user_id), so email collisions cannot report false success. New users copy the canonical fixture user's creation time by UUID when present; new accounts use their own tenant user's creation time. This reads no canonical account or credential, changes no canonical row or provider identity, and creates no session.

The development base seed uses a separate entry point for the canonical tenant when it uses native accounts:

TENANT_PRODUCTION=false tadoku-tenant canonical-development-accounts \
--admin-id <fixture-admin-uuid> --reader-id <fixture-reader-uuid>

It takes no tenant and always writes tadoku/prod; development-accounts keeps refusing that tenant, so the branch task cannot reach it. It has the same host, TENANT_PRODUCTION=false, fixture and insert-missing rules, in one transaction, and needs only users and accounts DML. scripts/dev/seed-db.sh runs it with the runtime role over a loopback port-forward only after the base API reports v2, so row-level security scopes every statement; see Seed data.

inspect accepts only parsed test keys and registry-management credentials. It prints {"tenant":"tadoku/branch-0123abcd","exists":false} after successful teardown. An absent registry row proves removal of its foreign-key-owned users, accounts, sessions, recovery tokens, emails and access tokens. Existing test rows return true; production-kind rows and unauthorized credentials fail closed.

Email access tokens​

Run the command attached to a cluster shell with the intended database credentials; never create tokens through a Kubernetes Job, redirected log, CI step or API. Token operations need only TENANT_POSTGRES_* and access-token table DML privileges, plus row-lock privileges on the tenant registry for creation. They make no Valkey or Flipt writes. Runtime row-level security still scopes each statement to the parsed tenant.

tadoku-tenant access-token create --tenant tadoku/branch-0123abcd \
--scope email:read --name email-debug --expires-in 24h
tadoku-tenant access-token list --tenant tadoku/branch-0123abcd
tadoku-tenant access-token revoke --tenant tadoku/branch-0123abcd --id <token-uuid>

Create prints one raw token line only after commit. Store it privately; it cannot be recovered. List returns JSON metadata (ID, Name, Scopes, CreatedAt, ExpiresAt, LastUsedAt, RevokedAt), never token values or hashes. Revoke uses the listed UUID, is tenant-scoped and is safe to repeat. A missing UUID is an error. Creation requires a nonblank name of at most 128 bytes, exactly email:read, and a positive Go duration no longer than 720h (30 days). At most ten unrevoked, unexpired tokens may exist per tenant; the tokens service sets that limit and the insert enforces it atomically. Concurrent creates serialize on that tenant's registry row; expired and revoked tokens release slots.

TENANT_PRODUCTION defaults to true, which refuses creation for tadoku/prod. Only a development deployment may explicitly set it to false. The API separately enforces its own production setting, so a token minted with an incorrect CLI setting cannot authorize production email reads. Other valid test tenant keys remain available in either deployment.

Tokens contain 256 random bits in tdk_at_<tenant-name>_<tenant-id>_<secret>; only the complete token's SHA-256 hash is stored. Use the token in an Authorization: Bearer header on /api/internal/emails or its message-detail route. Every other business route retains JWT authentication. The token selects its own tenant; it cannot select another tenant through a header, JWT or URL. A branch API refuses a token for any other tenant with 421.