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
| Option | Description | Default |
|---|---|---|
--orgName, --name | Organization display name. A tenant named <orgName> Tenant is created alongside it. | (required) |
--email | Primary superadmin email. | (required) |
--password | Primary 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, --slug | Optional 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) |
--roles | Comma-separated list of roles to ensure exist before assignment. | superadmin,admin,employee |
--skip-password-policy | Skip 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-users | Opt 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-examples | After 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 |
--json | Suppress 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
- Ensures each role in
--rolesexists (creating it if necessary). - 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.
- Otherwise creates:
- A tenant named
<orgName> Tenant. - An organization
<orgName>bound to the tenant. - A superadmin user for
--email. Derivedadmin@<domain>andemployee@<domain>demo accounts are created only when--include-demo-usersis passed; without it, no demo accounts are seeded.
- A tenant named
- Rebuilds the organization hierarchy for the tenant.
- Seeds default role ACLs:
- Superadmin role marked
isSuperAdminwith wildcard features. - Admin role receives broad feature coverage (
auth.*,entities.*,directory.organizations.*, etc.). - Employee role is granted dashboard and example-module features.
- Superadmin role marked
- Prints the resulting tenant, organization, and user IDs.
Example
yarn saasframe auth setup \
--orgName "Acme HQ" \
--email [email protected] \
--password ChangeMe123 \
--roles superadmin,admin,employee \
--include-demo-users
Output (abridged):
🎉 Created user [email protected] password: ChangeMe123
⚠️ GENERATED password — copy now; it is not stored in plain text
🎉 Created user [email protected] password: 8d5C-aZ1xQNvF0jK
⚠️ GENERATED password — copy now; it is not stored in plain text
🎉 Created user [email protected] password: r2Bp_kL9nQjHm-Cw
✅ Setup complete: { tenantId: '...', organizationId: '...' }
Without --include-demo-users, only the primary [email protected] 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 [email protected] \
--password ChangeMe123 \
--skip-password-policy \
--with-examples \
--json)
echo "$TENANT" | jq -r .tenantId
Output:
{"tenantId":"...","organizationId":"...","adminUserId":"...","adminEmail":"[email protected]","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.
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--passwordare 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-rolesran previously if you override the default role list. - Password policies – the command enforces the configured password policy unless you pass
--skip-password-policy. UpdateSF_PASSWORD_MIN_LENGTH,SF_PASSWORD_REQUIRE_DIGIT,SF_PASSWORD_REQUIRE_UPPERCASE, andSF_PASSWORD_REQUIRE_SPECIALto customize the rules.