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.