Tenants and users
Tenant (your organization: API keys, branding)
├── Users (id · tenantKey · displayName · email · phone · locale · timezone · units)
└── Flows · Sources · Contacts · Webhooks · RunsEvery record you supply (sources, adapters, flows, and users) carries the same identity pair:
a displayName shown in the GUI (change it any time) and a stable tenantKey, your
own identifier, unique per tenant, so your systems can keep referencing the record after a
rename (tiles use their key for this). tenantKey is optional everywhere except users.
- A tenant is your account with Growth Engine, the party that talks to the API, and the isolation boundary. Nothing is visible across tenants and there is no “all tenants” parameter anywhere in the API.
- A user is a person you register so they can use the Growth Lab GUI. Flows, sources, contacts and contact lists belong to a user: a signed-in user sees and edits only their own, and everything they create is theirs. Your backend (a tenant key) sees every user’s records and can act for a user. You manage users; the Engine does not run sign-up, passwords, roles or groups. Whatever hierarchy your platform has (customers, regions, permissions) stays in your platform; you decide which of your users may open Growth Lab and mint them a session token when they do.
- Each user has an Engine-assigned
id(GUID) and atenantKey, your identifier for that person (your user id), unique within your tenant. Use it to map Engine users back to your records.
What the Engine stores about a user
Only what Flow and Engine operation needs:
| Field | Purpose |
|---|---|
id | Engine GUID (stable primary key) |
tenantKey | your identifier for the user, unique per tenant |
displayName | shown in the GUI and in audit records |
email, phone (optional) | so the user can receive reports/notifications, only after verification (below) |
locale, timezone, units (optional) | UI language, report timestamps, °C/°F |
emailVerified, phoneVerified | verification status + timestamps |
canOverrideBranding | may this user set a personal brand for their own account (Branding) |
The Engine stores no passwords and no roles.
Who owns what
| Record | Owner | User session | Tenant key / tenant session |
|---|---|---|---|
| Flows, sources, contacts, contact lists | a user (ownerUserId / ownerUserKey) | sees and changes only its own; creates as itself | sees all; ?userKey= narrows a list; ownerUserKey on create or PATCH assigns (or null = private to the tenant, invisible to users) |
| Runs, reports | follow their flow | own flows only | all |
| Adapters, webhooks, branding, tiles, keys, users | the tenant | as gated today | all |
A flow’s sources and contact lists, and a list’s contacts, must belong to the same user as the
record you are saving (422 otherwise). tenantKey is unique per owner, so two users may both
register a field called F-12. The hourly targets sync runs once per user, so synced sources
belong to the user whose userKey the adapter received.
Who authenticates how
| Caller | Credential | Principal | Routes it may call |
|---|---|---|---|
| Your backend | Tenant API key (Authorization: Bearer glpk_…) | tenant | tenant- and user-gated routes (subject to the key’s scopes) |
| A GUI user | Session cookie, obtained by exchanging a session token you minted for that user | user | user-gated routes only, on records they own |
| Your GUI acting as the tenant | Session cookie from a tenant session token (POST /v1/session-tokens; no user, frozen to the minting key’s scopes) | tenant | same as the minting key |
| Soiltech operations | Admin API key (not tied to a tenant) | admin | everything (provisioning is admin-only) |
Every route declares one of these gates; see API authentication → gates and GUI authentication for the token flow.
Verified addresses
The Engine never sends to an email address or phone number it has not verified; this applies to users and to alert contacts. Verification is triggered by you (or from the GUI):
POST /v1/users/{id}/verifications/POST /v1/contacts/{id}/verificationswith{ "channel": "email" | "sms" }.- Email: the Engine sends a one-time link (valid 24 h) to a branded confirmation page.
- SMS: the Engine texts a 6-digit code (valid 10 min, 5 attempts); confirm with
POST …/verifications/confirm { "code": "123456" }(from your backend or the GUI dialog). - On success the record shows
emailVerified/phoneVerifiedand the Engine emits auser.verified/contact.verifiedwebhook. Changing an address resets its verification. - Sends to unverified addresses are skipped and recorded (
skipped_unverified), never silently dropped.