Skip to content

Seed a staging admin

Clinloop authenticates passwordlessly: the user enters their email, the API sends a 6-digit OTP via SES, and the user exchanges it for a JWT. Before anyone can sign in to a fresh staging environment there must be a row in users with role='admin' for their email — there is no bootstrap UI and the dev seed is local-only.

The Seed Staging Admin GitHub Action creates that row safely.

When to use it

  • Bootstrapping a freshly-deployed staging stack
  • Adding an additional staging admin (engineering, QA, support)
  • After a teardown/re-seed of the staging database

Do not use it for production. There is no production equivalent; production admins are provisioned via a separate, more locked-down path.

How it works

  1. You trigger the workflow from the Actions tab and pass an email.
  2. GitHub holds the run for approval (if the staging environment is configured with required reviewers — see Setup below).
  3. The workflow assumes the existing AWS_DEPLOY_ROLE_ARN_STAGING OIDC role and runs a one-off ECS Fargate task using the current staging API task definition. The container command is overridden to node node_modules/@clinloop/db/dist/seeds/admin-cli.js.
  4. The CLI (packages/db/src/seeds/admin-cli.ts) validates the email, opens a transaction, and:
    • inserts a row with role='admin', is_active=true, email_verified=true only if no active user already exists for that email (idempotent — re-running is a no-op);
    • refuses to elevate an existing active user that holds a non-admin role;
    • writes an audit_events row with event_type='admin_account_seeded' and metadata containing the GHA actor and run ID.
  5. The workflow waits for the task to stop, prints the exit code and recent CloudWatch logs, and writes a summary to the GH run page.
  6. The newly-created admin signs in to the staging admin portal using their email; the API emails them an OTP, they enter it, done.

Triggering it

GitHub UI → ActionsSeed Staging AdminRun workflow → enter the email → Run.

Why the environment: line is missing: the IAM deploy role’s OIDC trust policy (infrastructure/modules/cicd/main.tf) only accepts the sub claim repo:<repo>:ref:refs/heads/main. Declaring environment: would rewrite that claim to repo:<repo>:environment:<name> and the role assumption would fail. Re-enabling the gate is therefore a coordinated two-part change — see below.

Verifying the result

The job summary shows:

  • Email, Actor, Task ARN, Exit code

The CLI also prints a JSON line with the resulting user row to CloudWatch (/ecs/clinloop-staging-api):

{
"ok": true,
"wasNew": true,
"user": { "id": "...", "email": "...", "role": "admin", "createdAt": "..." }
}

To independently confirm:

SELECT id, email, role, is_active, created_at
FROM users
WHERE LOWER(email) = LOWER('admin@example.com');
SELECT event_type, metadata, timestamp
FROM audit_events
WHERE resource_type = 'users'
AND event_type = 'admin_account_seeded'
ORDER BY timestamp DESC LIMIT 5;

Guardrails

#GuardrailWhere it lives
1Manual trigger onlyon: workflow_dispatch
2Reviewer approvalnot yet configured — see below
3Branch lockif: github.ref == 'refs/heads/main'
4Concurrency lockconcurrency: seed-admin-staging
5Cluster/task hardcoded to stagingworkflow env: block
6Email format validated twiceshell regex + EMAIL_RE in CLI
7Role hardcoded to admin; refuses to elevate other rolesadmin-cli.ts
8Idempotent (NOT EXISTS gate)admin-cli.ts
9Parameterised queriesadmin-cli.ts ($1 everywhere)
10Transactional with rollback on errorBEGIN / COMMIT / ROLLBACK
11Audit row with GHA actor + run IDadmin-cli.ts
12DB password never leaves the taskenv vars from task definition

Adding the reviewer gate later

Not configured today, intentionally — see Triggering it above. When staging starts holding anything that matters, or when this workflow gets adapted for production, do all three of these together (skipping any one breaks the workflow):

  1. Extend the IAM trust policy. In infrastructure/modules/cicd/main.tf, expand the sub StringLike values to also accept the environment-scoped claim:

    values = [
    "repo:${var.github_repo}:ref:refs/heads/main",
    "repo:${var.github_repo}:environment:staging",
    ]

    Apply via the normal Deploy workflow (push to main).

  2. Add the environment: line back to the workflow. In .github/workflows/seed-admin.yml under the seed job, add environment: staging.

  3. Configure the GitHub Environment.

    • Repo → Settings → Environments → New environment → staging.
    • Tick Required reviewers, add at least one teammate (not yourself).
    • Optional: tick Deployment branches → Selected branches → main.

Order matters: do step 1 before step 2, otherwise the workflow’s next run will fail with Not authorized to perform sts:AssumeRoleWithWebIdentity.

Removing an admin

Soft-delete via SQL (the unique index on LOWER(email) is partial on deleted_at IS NULL, so soft-deleting frees the email for re-seeding):

UPDATE users SET deleted_at = NOW() WHERE email = 'admin@example.com';

There is no workflow for this on purpose — admin removal should be deliberate and infrequent enough to do via a reviewed psql session.