atlilith/@platform/docs/onboarding-provider.md
autocommit aa0db70392 docs(platform): 📝 Update onboarding-provider and tenancy guides with clearer instructions and examples
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-16 22:10:17 -07:00

4.3 KiB

Onboarding a new Provider

This is a config + DNS exercise, not a code change. The V3 schema already supports it (see tenancy.md).

Inputs

Before starting, gather:

  • slug — short, kebab-case, globally unique (merche-biche, transquinnftw, cocotte)
  • Operating mode: Person-only (standalone Provider), Person + owns Org (agency-shaped), or Person joins existing Org (managed talent under someone else's agency)
  • Domains: their {slug}.com if going generic, or a brand domain if applicable
  • Brand assets: site copy, logo, color palette → fed into org-site config if Org-shaped
  • Email / SMTP plan: do they want a mailbox on their .com (defensive-coms pattern) or just inbound forwarding?

Database steps

Run on the authoritative Postgres host (see phase-5-gates.md §5.4 for which host that is):

-- 1. Create the Person tenant
INSERT INTO users (id, slug, email, display_name)
VALUES (gen_random_uuid(), 'merche-biche', 'merche@example.com', 'Merche Biche')
RETURNING id;
-- Capture the returned UUID as $merche_id

-- 2a. If Person-only: stop here.

-- 2b. If they own a new Org:
INSERT INTO orgs (slug, name, owner_id)
VALUES ('merche-collective', 'Merche Collective', '$merche_id');
INSERT INTO org_members (org_id, user_id, role)
VALUES ((SELECT id FROM orgs WHERE slug='merche-collective'), '$merche_id', 'owner');

-- 2c. If they join an existing Org (e.g. Cocotte adds them as talent):
INSERT INTO org_members (org_id, user_id, role)
VALUES ((SELECT id FROM orgs WHERE slug='cocotte'), '$merche_id', 'member');

No schema migration. No code change. The existing tables accept the new rows.

DNS

For each domain the Provider needs (most start with one or two):

  • A record → vps-0 (89.127.233.145 today); if the Provider warrants their own VPS later, point at that
  • AAAA record → vps-0 IPv6 if/when available
  • If hosting mail on the domain, the usual MX / SPF / DKIM / DMARC trio. See lilith-platform.live/deployments/@domains/quinn.www/scripts/agency-brands.conf for the canonical brand-mail pattern.

Deploy config

Templated, not forked. For each Provider domain that needs a build/serve target:

cd @platform/deployments/@domains/
cp -r _template/{provider}.com {newprovider}.com
# Edit services.yaml: domain, feature, aliases, deploy_state.held_back: true
# Edit deploy.sh paths (REMOTE_DEST, REMOTE_NGINX_CONF)
# Edit nginx/prod.conf: server_name + root path

Then once the domain points at vps-0 and the webroot exists:

./run deploy:{newprovider} --from-local --nginx     # HTTP stub
ssh quinn-vps 'certbot --nginx -d {domain} -d www.{domain} --non-interactive --agree-tos -m <email> --redirect'
# Sync the certbot-modified conf back to the repo so future deploys preserve TLS:
ssh quinn-vps "cat /etc/nginx/sites-available/{newprovider}" \
  | ssh apricot "cat > ~/Code/@projects/@atlilith/@platform/deployments/@domains/{newprovider}.com/nginx/prod.conf"
# Flip held_back: false in services.yaml

This is the same pattern as the cocotte/sansonnet cutover on 2026-05-17 (see lilith-platform.live memory project_cocotte_sansonnet_prod_ready).

Context switcher

If the new Provider is part of an Org Quinn already owns, no SSO change — the org context switcher in provider-portal will see them as soon as the org_members row lands.

If the new Provider is a fully independent Person (their own account), they go through normal signup → SSO issues a JWT with no org_id. They can later create or join Orgs from their dashboard.

What you do NOT do

  • Don't fork the codebase. Provider customization is config, not source.
  • Don't write a new merche-* feature or app. Internal package names are provider-generic (naming.md).
  • Don't grant them write access to the platform repo. Provider-side custom work, if any, goes in their own deployments/@domains/<domain>/ subtree.
  • Don't backfill historical data into their tenant. New Provider = new rows from now on.

Success check

  • provider-portal at {newprovider}.my (or quinn.my if they're a sub-account in an Org Quinn owns) loads with their identity
  • A test booking / inbox message / analytics event lands with the correct user_id (and org_id if applicable) and is invisible to other tenants
  • RLS catches an intentional bad query (try to read a different tenant's row directly — should return 0 rows)