Href Creative Docs
HREF Team Docs

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
ArgumentFormatPurpose
<slug>lowercase letters, digits, hyphens; 1-63 charsBecomes the subdomain (acme.hrefcreative.co) and is permanent
<Display Name>any string, quoted if it has spacesShown on the splash and in notification emails
<admin-email>valid email addressReceives 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):

  1. tenants row.

    • slug, name
    • api_key_hash: a SHA-256 hash of a freshly generated key. The plaintext key is hcrm_<43 random base64url chars>.
    • settings: '{}', default values for the rest.
  2. Default pipeline stages. Calls the SQL function seed_default_pipeline_stages(tenant_id) which inserts these five stages:

    sort_ordernamecolorstage_type
    1New#3b82f6active
    2Contacted#a855f7active
    3Qualified#06b6d4active
    4Won#10b981won
    5Lost#ef4444lost
  3. Admin invite. Inserts a tenant_invites row 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-Key header 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:

  1. Confirm the slug they want (and that it is available).
  2. Run provision-tenant.
  3. Save the API key to 1Password under "HREF CRM / API keys".
  4. Email the admin invite link.
  5. Hand the API key (separately, with care) to the HREF developer wiring up their lead form.
  6. 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.

On this page