Every paying customer is a tenant. Each tenant gets their own database, their own worker, their own WhatsApp number. A shared orbis-admin control plane keeps track of all of them but never holds a tenant's conversation data.
orbis-admin
Fleet control plane · 1 shared DB
tenants
tenant_users
tenant_channels
tenant_deployments
usage_daily, invoices
admin_users (role-gated)
orbis_prospects (sales)
vps_hosts
stripe_events, oauth_states
orbis-dreamlink
Agency vertical
Customer #1 · live 2026-04-24 · 169 contacts, 3,168 conversations
orbis-nova
Travel-concierge
Queued for Phase 2B
orbis-gaku
Agency vertical
First external paying customer · onboarding
orbis-{future}
Any vertical
Every future customer — same shape
Every tenant DB has the same core schema plus one vertical overlay. Zero cross-tenant data. Zero shared tables between customers.
Inside one tenant's database
Three stacked layers. The bottom is universal, the middle is industry-specific, the top is bespoke. A tenant can opt out of the upper two — a plain Orbis with zero verticals works fine.
Custom layer
Bespoke modules for enterprise customers whose workflows don't fit any template. Priced as professional services. Empty for most tenants.
(tenant-specific)
Vertical layer
Industry overlay — agency, travel-concierge, family-office, fund-ops. Each vertical ships schema + tools + prompts + config as a versioned artifact. One vertical per tenant at a time.
Shown: agency vertical · travel-concierge would show trips / bookings / celebrations / households instead.
Core layer
Universal — every Orbis tenant has this schema. Maintained once by Dreamlink, shipped to every customer. Distilled 2026-04-24 from 82 → 62 tables by dropping dead / redundant / patch-stacked schemas and consolidating three dedup tables into one and two state tables into orbis_state.
One table — contacts — is the canonical person record. Everywhere else in the schema where a person appears, the table holds a contact_id pointing back at contacts.id. No parallel users table. No WhatsApp LID stored as identity anywhere.
conversations
→ contact_id
Every message (WhatsApp / Slack / Discord / email) logged back to the sender, unless the sender is an unresolved orphan (NULL + sender_name preserved).
contacts
id · name · phone · email · whatsapp_jid · slack_id · discord_id · role · tier · energy_state
One row per person. Absorbed the old users table. Phone is the canonical identifier for routing. WhatsApp LIDs are never stored.
memory
→ contact_id
Structured facts about a person — preferences, last interaction, operator-level metadata.
Reminders and recurring actions for / about a contact.
group_messages
→ sender_contact_id
Group-chat messages resolved to the sender contact when identity can be determined from membership.
deals (vertical)
→ contact_id
Agency vertical — every deal is tied to the primary contact on the other side.
talent_profiles (vertical)
→ contact_id
Agency vertical — every talent on the roster is a contact first, talent second. One talent profile per contact.
household_members (travel vertical)
→ contact_id
Travel vertical — every household member is a contact. Households are groupings over real people, not independent identities.
containers
→ member_contact_ids (JSON)
Group chats reference their members as contact ids. When someone leaves a group, their contact stays intact — they just stop appearing in the membership array.
group_unknown_contacts
→ resolved_contact_id
Unresolved senders get pointed at a real contact once classified via the A-F/N menu. Until then, Orbis keeps them outside the trusted context.
If you ever touch a person anywhere in the system — sending them a message, adding them to a deal, logging a meeting, booking a trip for them — it lands in contacts first. Everything else references.
Core tables in detail
A closer look at what sits in each box, with the columns that matter for understanding the FK relationships.
Identity & access
contacts
id (PK) name · phone · email whatsapp_jid · slack_id contact_type · role relationship_tier · tier_score energy_state merged_into → contacts.id
id (PK) contact_id → contacts sender_name (when orphan) role · message channel · product_context timestamp
group_chats
group_jid (PK) group_name stage
group_messages
id (PK) group_jid sender_contact_id → contacts message · orbis_response
slack_messages
id (PK) channel_id sender_contact_id → contacts message · thread_ts
Memory, knowledge, actions
memory
id (PK) contact_id → contacts type · key · value visibility · sensitive expires_at
knowledge
id (PK) scope · section content
meetings
id (PK) calendar_event_id title · attendees start_time · end_time summary
scheduled_actions
id (PK) contact_id → contacts action_type scheduled_for title · body · recurrence
interactions
id (PK) contact_id → contacts interaction_type channel · direction summary
behavioral_rules
id (PK) rule status
Containers & isolation (multiplayer protocol)
Every group chat is a walled garden. Knowledge shared inside a container stays in the container unless an explicit container_share audit trail is written. Orbis never silently crosses a boundary.
containers
id (PK) platform · name container_type member_contact_ids (JSON) sensitivity last_active_at
container_shares
id (PK) knowledge_id from_container_id → containers to_container_id → containers requested_by_contact_id → contacts approved_by_contact_id → contacts status · resolved_at
group_unknown_contacts
id (PK) group_jid · sender_jid sender_phone · push_name resolved_name resolved_contact_id → contacts status · nudge_count
access_permissions
contact_type · contact_id can_query_contacts can_query_sensitive can_send_as_orbis (see also: identity section)
Trust layer (every tool call is audited)
executions
id (PK) tenant_id · workflow_type user_input normalized_intent final_output · status
execution_steps
id (PK) execution_id step_index · step_type input_json · output_json status
execution_claims
id (PK) execution_id · step_id claim_key source_type · source_ref verification_status
id (PK) contact_id → contacts scout_source scorecard_* decision
talent_positioning_history
id (PK) talent_id → talent_profiles field_name previous_value · new_value rationale
Current state of orbis-dreamlink
75tables
169contacts
3,168conversations
94%identity-resolved
Phase 2A — identity layer migrated
169 contacts copied from nova-leon to orbis-dreamlink, schema upgraded in-flight
63 legacy users rows auto-linked via phone / Slack / unambiguous name matching
2,973 conversations (94%) carry a contact_id pointing to a real person
195 orphan conversations preserved with contact_id = NULL and sender_name; not deleted, not fake-attributed
2 merged-contact relationships preserved
0 WhatsApp LIDs migrated — the lid_jid column no longer exists anywhere in the new schema
What's next
Phase 2B — migrate knowledge, deals, PM, meetings, trust-layer data into orbis-dreamlink. Phase 3 — flip the nine workers that currently read nova-leon to read orbis-dreamlink instead, one at a time, with dual-write windows. Phase 4 — extract dreamlink-ops (agency PM/staff for Dashboard) and nova-db (travel product) out of orbis-dreamlink. Phase 5 — drop nova-leon and provision the first external tenant (Gaku) against the proven architecture.