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}.comif going generic, or a brand domain if applicable - Brand assets: site copy, logo, color palette → fed into
org-siteconfig 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.145today); 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.conffor 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-portalat{newprovider}.my(orquinn.myif 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(andorg_idif 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)