Skip to main content

saasframe auth setup

yarn saasframe auth setup provisions the initial tenant and organization, seeds user accounts, and configures baseline role ACLs. It is safe to run multiple times; existing entities are reused and updated idempotently.

Usage

yarn saasframe auth setup --orgName "<organization>" --email "<superadmin@email>" --password "<password>" [--orgSlug <slug>] [--roles superadmin,admin,employee] [--skip-password-policy] [--include-demo-users] [--with-examples] [--json]

Aliases: --name for --orgName. --slug for --orgSlug.

Options

OptionDescriptionDefault
--orgName, --nameOrganization display name. A tenant named <orgName> Tenant is created alongside it.(required)
--emailPrimary superadmin email.(required)
--passwordPrimary superadmin password. Derived admin@…/employee@… accounts (only created when --include-demo-users is set) receive their own passwords — SF_INIT_ADMIN_PASSWORD / SF_INIT_EMPLOYEE_PASSWORD when set, otherwise an autogenerated 16-char base64url string surfaced in stdout.(required)
--orgSlug, --slugOptional slug persisted on the new organization. Triggers a best-effort uniqueness pre-check via findOneWithDecryption(Organization, { slug }): the DB unique constraint is per-tenant ((tenant_id, slug)), so the pre-check is race-safe within a tenant and advisory across tenants — two concurrent invocations could both pass and yield two organizations sharing the slug in different tenants. Also forces a fresh-tenant signal: an existing user with --email aborts with a clear error rather than silently reusing the foreign tenant. Format: lowercase, digits, dashes (1–63 chars; cannot start or end with a dash).(unset)
--rolesComma-separated list of roles to ensure exist before assignment.superadmin,admin,employee
--skip-password-policySkip password policy validation for the primary user (useful for non-interactive bootstrap of demo tenants). Derived demo-user passwords are not subject to the policy.off
--include-demo-usersOpt in to seeding the derived admin@<domain> and employee@<domain> demo accounts. Default-off: standalone callers no longer get these accounts unless they ask for them. In production, omitting both this flag and the env overrides is the safe path; passing this flag without the env overrides surfaces autogenerated passwords on stdout.off
--with-examplesAfter tenant creation, run every enabled module's seedExamples lifecycle hook (mirrors saasframe init's opt-out example data, but opt-in here so production callers don't accidentally seed demo data).off
--jsonSuppress banners/progress on stdout and emit a single JSON line at the end with tenantId, organizationId, adminUserId, adminEmail, and reusedExistingUser. Sets SF_CLI_QUIET=1 automatically and silences console.log/console.info for the duration so consumers can pipe directly into jq.off

Behavior

  1. Ensures each role in --roles exists (creating it if necessary).
  2. Checks for an existing user with the supplied email:
    • If found, updates their password, tenant, organization, and role assignments without creating duplicates.
    • Logs a warning indicating that the existing account was reused.
  3. Otherwise creates:
    • A tenant named <orgName> Tenant.
    • An organization <orgName> bound to the tenant.
    • A superadmin user for --email. Derived admin@<domain> and employee@<domain> demo accounts are created only when --include-demo-users is passed; without it, no demo accounts are seeded.
  4. Rebuilds the organization hierarchy for the tenant.
  5. Seeds default role ACLs:
    • Superadmin role marked isSuperAdmin with wildcard features.
    • Admin role receives broad feature coverage (auth.*, entities.*, directory.organizations.*, etc.).
    • Employee role is granted dashboard and example-module features.
  6. Prints the resulting tenant, organization, and user IDs.

Example

yarn saasframe auth setup \
--orgName "Acme HQ" \
--email superadmin@acme.dev \
--password ChangeMe123 \
--roles superadmin,admin,employee \
--include-demo-users

Output (abridged):

🎉 Created user superadmin@acme.dev password: ChangeMe123
⚠️ GENERATED password — copy now; it is not stored in plain text
🎉 Created user admin@acme.dev password: 8d5C-aZ1xQNvF0jK
⚠️ GENERATED password — copy now; it is not stored in plain text
🎉 Created user employee@acme.dev password: r2Bp_kL9nQjHm-Cw
✅ Setup complete: { tenantId: '...', organizationId: '...' }

Without --include-demo-users, only the primary superadmin@acme.dev user is created — admin@… / employee@… are no longer seeded silently. To pin specific passwords for the derived demo accounts (so output stays deterministic in CI), set SF_INIT_ADMIN_PASSWORD and SF_INIT_EMPLOYEE_PASSWORD before invoking the command.

Production safeguard

In production (NODE_ENV=production), passing --include-demo-users without setting both SF_INIT_ADMIN_PASSWORD and SF_INIT_EMPLOYEE_PASSWORD aborts the command with exit code 2 and a DERIVED_USER_PASSWORD_REQUIRED error on stderr. Either set the env vars to operator-chosen secrets or omit --include-demo-users and let the operator provision admin/employee users via saasframe auth add-user afterward.

Scriptable provisioning (--orgSlug + --json)

For staging seeding loops, sales-engineering demo provisioning, customer onboarding, or DR restores, combine --orgSlug with --json to get a single-line JSON contract on stdout:

TENANT=$(yarn saasframe auth setup \
--orgName "Acme HQ" \
--orgSlug acme-staging-42 \
--email admin@acme.test \
--password ChangeMe123 \
--skip-password-policy \
--with-examples \
--json)

echo "$TENANT" | jq -r .tenantId

Output:

{"tenantId":"...","organizationId":"...","adminUserId":"...","adminEmail":"admin@acme.test","reusedExistingUser":false}

When --orgSlug collides with an existing organization slug, the command exits 1 and writes ORG_SLUG_EXISTS: an organization with slug "<slug>" already exists to stderr instead of clobbering the existing org. When --orgSlug is set and --email matches an existing user, the command exits 1 with Setup aborted: user already exists with the provided email. rather than silently reusing the foreign tenant.

Slug uniqueness scope

The pre-check is a best-effort guard, not a global uniqueness contract. The underlying DB constraint on organizations is (tenant_id, slug) (per-tenant), so two concurrent saasframe auth setup --orgSlug=foo invocations creating new tenants can both pass the application-level check and both succeed — the end state is two organizations sharing the slug in different tenants. For a one-time bootstrap this race is unlikely, but downstream tooling that depends on the slug as a stable cross-tenant handle should either (a) serialize provisioning or (b) add a partial unique index on slug alone in a follow-up migration.

Troubleshooting

  • Missing options – the command prints the usage line and exits if --orgName, --email, or --password are not provided.
  • Existing data – rerunning the command against an existing tenant will update user passwords and assignments rather than duplicating records.
  • Role mismatches – ensure saasframe auth seed-roles ran previously if you override the default role list.
  • Password policies – the command enforces the configured password policy unless you pass --skip-password-policy. Update SF_PASSWORD_MIN_LENGTH, SF_PASSWORD_REQUIRE_DIGIT, SF_PASSWORD_REQUIRE_UPPERCASE, and SF_PASSWORD_REQUIRE_SPECIAL to customize the rules.