Provisioning a tenant
In v1, tenant creation happens by running a CLI script. There is no admin UI yet (Phase 1.5+).
The command
npm run provision-tenant -- <slug> "<Display Name>" <admin-email>
Concrete example:
npm run provision-tenant -- acme "Acme Family Law" admin@acmefamilylaw.com
| Argument | Format | Purpose |
|---|---|---|
<slug> | lowercase letters, digits, hyphens; 1-63 chars | Becomes the subdomain (acme.hrefcreative.co) and is permanent |
<Display Name> | any string, quoted if it has spaces | Shown on the splash and in notification emails |
<admin-email> | valid email address | Receives the first invite; becomes the workspace's first admin |
What gets created
The script (in scripts/provision-tenant.ts) runs in this order, all using the service-role Supabase client (RLS bypassed for trusted server-side paths):
-
tenantsrow.slug,nameapi_key_hash: a SHA-256 hash of a freshly generated key. The plaintext key ishcrm_<43 random base64url chars>.settings: '{}', default values for the rest.
-
Default pipeline stages. Calls the SQL function
seed_default_pipeline_stages(tenant_id)which inserts these five stages:sort_order name color stage_type 1 New #3b82f6active 2 Contacted #a855f7active 3 Qualified #06b6d4active 4 Won #10b981won 5 Lost #ef4444lost -
Admin invite. Inserts a
tenant_invitesrow with:email: the admin email (lowercased)token: 32 random bytes (base64url)role: 'admin'expires_at: now + 7 days
What the script outputs
On success, you see a block like this. Copy the API key now. It is hashed in the database; we cannot show it to you again.
===========================================
Tenant provisioned: Acme Family Law
===========================================
Slug: acme
Tenant ID: 79732b65-ec7d-4e9e-91f7-90bc42a0f763
--- ONE-TIME ---
API key (store securely; not retrievable again):
hcrm_AJoMq2B4SvJDvshNJqruPrZOIudmVh1r1QkwNuYJU4M
--- LINKS ---
Splash: http://acme.localtest.me:3001/
Admin invite: http://acme.localtest.me:3001/invite/4v5PyyE2qx4keXqjLv1ZmTGz9bCTwg6g4aP6IBw9AUs
Intake URL: http://acme.localtest.me:3001/api/leads/intake
--- TEST INTAKE ---
curl -X POST http://acme.localtest.me:3001/api/leads/intake \
-H "Content-Type: application/json" \
-H "X-API-Key: hcrm_AJoMq2B4SvJDvshNJqruPrZOIudmVh1r1QkwNuYJU4M" \
-d '{"first_name":"Test","email":"test@example.com","source":"manual-test"}'
The URLs use whichever NEXT_PUBLIC_APP_DOMAIN and NEXT_PUBLIC_APP_PROTOCOL are set in .env.local. Run the script with the production env vars when provisioning real tenants.
Sending the invite to the admin
For now, copy-paste the Admin invite URL and email it from your own inbox. We do not yet have automated invite emails (lands in Phase 7's Settings → Team work).
When the admin clicks the link, they pick a password, and they are signed in. Their tenant_members row is created at that moment with role: 'admin'.
API key handling
- Generated once at provisioning time. Plaintext is shown only in the script's output.
- Stored as SHA-256 of the plaintext. Cannot be reversed.
- Used by HREF developers in the
X-API-Keyheader on every request to/api/...for that tenant. - Rotation: lands in Phase 7 settings. The mechanism: generate a new key, update
tenants.api_key_hash, return the plaintext to the user once. Old key stops working immediately.
If you lose a key before rotation lands, the workaround is to UPDATE tenants.api_key_hash directly in Supabase Studio with the SHA-256 of a new value you generate yourself. Treat that as a band-aid until the UI ships.
Adding more team members
After the admin accepts their invite, they invite teammates from Settings → Team (Phase 7). The flow is identical: create a tenant_invites row, send them the link, they accept, they get a tenant_members row.
In v1, all members are role: 'member'. Roles are baked into the schema but are not yet differentiated in the UI; they activate in v2.
Removing a tenant
DELETE FROM tenants WHERE slug = '<slug>';
That cascades through tenant_members, tenant_invites, pipeline_stages, leads, lead_notes, lead_activities, and tasks. Auth users are left alone (a single user might belong to multiple tenants over their lifetime).
To also delete the auth users with no other memberships, you'd need a manual cleanup. We have not built that script because it's not needed for v1.
Provisioning checklist (manual)
When onboarding a new client:
- Confirm the slug they want (and that it is available).
- Run
provision-tenant. - Save the API key to 1Password under "HREF CRM / API keys".
- Email the admin invite link.
- Hand the API key (separately, with care) to the HREF developer wiring up their lead form.
- After the admin accepts, customize the workspace from Settings: logo, primary color, tagline, pipeline stages.
A real provisioning UI lives in Phase 1.5 / 7.