- Which notifications exist, and whether each is on, off, or mandatory.
- Who receives them, by role.
- Which channels they go out on, and how often.
Example app. Keystone’s source code is coming soon.
Keystone architecture overview
Keystone is a project-management product. It serves several customers — Acme and Globex, for example — each running multiple projects, and keeps each customer’s notifications entirely separate. What Keystone notifies users about
Who can change what
- An Admin sets the defaults across the account: which notifications exist, whether each is on, off or mandatory, who receives it by role, the digest cadence, conditions and channels. An admin also sets the customer’s branding and edits the message content.
- A Project Manager decides who is on their project and in which role. They set the project’s default notification settings, and can adjust individual members’ preferences across the project — as can an admin.
- A Project Sponsor, Process Owner or Team Member controls only their own notifications: a default that applies across all their projects, and an override for any single project.

Keystone concept mapping to SuprSend
Here’s how the Keystone concepts map to SuprSend:
Data modelling in SuprSend
1. Workspace
Each customer gets its own workspace. All of Acme’s assets and projects live in Acme’s workspace; Globex has its own and shares nothing with it.
- Notification categories, templates, and workflows
- Vendors, users, and preferences
- API keys and logs
dev— build and experiment.staging— test safely, with Test Mode holding back delivery to real users.prod— live sends.

acme-staging and acme-prod, globex-staging and globex-prod.
To keep more than one workspace in sync — for example Staging and Production — replicate categories, templates, and workflows with the SuprSend CLI.
A customer doesn’t always need to be modelled as a workspace. You can model each customer as a tenant, with its projects as sub-tenants. See tenants vs workspaces to choose.
2. Projects as tenants
Each of Acme’s projects is a tenant.acme-aero and acme-bms are two tenants inside Acme’s workspace. A project’s tenant_id is the project’s id.
A tenant is a segment inside a workspace — an organization, a team, or a project. SuprSend applies its configuration when the trigger passes its tenant_id.
A project’s tenant holds its preferences (see Customizing preferences) and, if it needs one, its own template variant (see Templates).
Keystone creates a tenant for each project through the Create/update tenants API:
default tenant — here, Acme’s own branding and settings. SuprSend applies it whenever a notification is triggered without a tenant_id, and any project tenant falls back to it for anything the project doesn’t set itself. Keystone’s projects set no branding of their own, so every message carries Acme’s.

A tenant can also carry per-project user properties — different channel identities or properties for the same user, per project (user-tenant mapping). Keystone doesn’t use it; a user’s role lives on the project’s object.
3. Projects as objects and their subscriptions
Each project is also an object — the record of who is on it. Users subscribe to the object, and a workflow triggered on it fans out to every subscriber, so a notification reaches the whole project without listing anyone in the trigger. Keystone creates the object with the Create/update objects API, then adds each user with the subscription API. Their role is stored as a subscription property:
Projects and roles in Keystone's Acme project
$recipient.subscription.role. A trigger names the object as the recipient and passes the project’s tenant_id:

Objects in SuprSend
An object can subscribe to another object, so you can model nested structures — a department containing several teams, say — where a trigger on the parent fans out across levels, up to two by default. Keystone’s projects are flat, so it isn’t used.
4. Notification categories
Every Keystone notification belongs to a notification category — the tree that holds a customer’s notification settings. It has three levels, and only the bottom one carries preferences:- Root — System, Transactional, or Promotional. Fixed; Keystone puts every notification under Transactional.
- Section — an optional grouping for the preference page. Carries no settings.
- Sub-category — one per notification. This is what a user opts in and out of, and where the customer’s defaults live. Each workflow is assigned one sub-category’s slug, and several workflows can share the same sub-category.

- Default preference — On, Off, or Can’t Unsubscribe (must-send). Channel-level opt-in/out still applies unless a channel is marked mandatory.
- Default channels — email, in-app inbox and Slack.
- Digest schedule — an optional cadence a user can pick, such as Instant, Daily, or Weekly, that drives a Digest node at send time.
- Condition properties — values the workflow reads at send time as
$category.properties.<key>. Keystone uses them for recipient rules — which roles receive the notification (role), plus specific users to always notify (include) or exclude (except), each a multi-select list of the workspace’s users — and for thresholds such as a cost limit.
- Customer-wide, for an admin — changes are stored on the category itself, as its default preference.
- Per project, for a project manager or an admin — changes are stored against that project’s tenant, so one project’s changes never affect another’s.

5. Customizing preferences
A preference decides whether someone receives a notification for a category, and on which channels. Three layers set it: a user’s own choice wins over the project’s default, which wins over the customer-wide default. Each layer is set in a different place:
Every layer sets the same four controls:
- Whether it’s sent — On, Off, or Can’t Unsubscribe.
- Channels — which ones the notification goes out on.
- Digest cadence — how often batched notifications arrive.
- Condition properties —
role,include,except, and thresholds like cost.
- A user’s Notifications tab, for their own preferences.
- Admin screens for the customer-wide defaults, a project’s defaults, and any user’s preferences on a project.
- If you’d rather not build the end-user preference centre, SuprSend ships prebuilt and headless preference components.

6. Workflows and conditions
A workflow is what runs when a notification fires — its steps, channels, and conditions. Each is linked to one notification category and to a template per channel. Triggering. Keystone triggers a workflow two ways, each passing the project’stenant_id:
- Direct API trigger — Keystone names the workflow, its recipients, and the data. The recipient is the project object, so SuprSend fans the notification out to everyone on that project (see Projects as objects) — the backend never has to look up or list members. It can also name a single user instead, for something aimed at one person like a task assignment.
- Event-based trigger — Keystone emits what happened, and every live workflow linked to that event fires, with SuprSend resolving the recipient. One product action can drive several notifications, and adding another one later is a dashboard change, not a backend change.
tenant_id in the trigger decides which project’s settings apply.
Preferences and recipient matching. SuprSend first drops anyone opted out of the workflow’s category. The trigger step then filters what’s left against the category’s recipient rules:
role and they aren’t in except, or if they’re in include. That’s how one workflow reaches only the project manager for a completed task, but the whole team for a started project.
Steps used. Beyond a plain send, Keystone’s workflows use two nodes:
- Wait Until — in
Tasks Coming Up, the reminder waits for whichever comes first: if the task is closed, nothing sends; if the due date comes within three days, the reminder goes out. - Digest — in
Comment Added, comments collect into one summary instead of a message per comment. Its schedule type is Preference Category, so it uses the cadence the recipient picked, falling back to the project default, then the category default, in the recipient’s timezone.

7. Templates
A template holds a notification’s content for each channel — email, in-app inbox, and any others. Content is Handlebars, with variables from the trigger, the recipient, and the tenant. The{{$brand.*}} variables are filled from the tenant at send time. See the full variable list.
Keystone creates and publishes templates through the Templates Management API — create the template, set each channel’s content, then commit a version.
Exposing templates in your app. Admins edit a notification’s content inside Keystone with the embedded template editor, authenticated with a scoped token. There’s one template per notification, so an edit applies to every project.

Keystone uses one default template per notification. If a project needs different content, add a variant scoped to its tenant.
8. Vendors
A vendor is the delivery provider that sends — email, SMS, push, or chat. Vendors are configured once per workspace, so every project sends through the customer’s own providers, under one sender identity. Keystone’s templates are authored for email, in-app inbox and Slack. Adding a vendor for another channel makes that channel available to the same templates.
A project can send from its own identity — a different email domain or SMS sender — with a tenant vendor. Keystone doesn’t use this.
End-to-end notification flow
With the categories, templates, workflows and vendors in place, one trigger runs the whole thing. Take a completed task on the Aerospace project: it should reach only that project’s manager. Keystone triggers the workflow with the project object as the recipient and the project astenant_id:
What a project keeps to itself, and what it shares
Everything for a customer lives in its own workspace. Within it:Other common scenarios
The same model extends beyond projects:- Units other than projects. Departments, teams, or regions map the same way — each a tenant and an object.
- One user across many projects. A user exists once in the workspace and subscribes to several objects, with a different role and different preferences on each.
- If your product is B2B2C and you’re looking to notify your customer’s end-consumers, see Designing notifications for B2B2C applications.
- Either way you still choose how to model a customer: as a tenant in one shared workspace, or as its own workspace, fully isolated (used here). The tenants vs workspaces overview compares the two.
Reference: credentials and APIs
Keystone talks to SuprSend across four surfaces. Secrets stay server-side; the two browser SDKs only ever get a short-lived JWT minted by the backend.Credentials
The frontend embeds authenticate the end user with a signed user token — an
ES256 JWT the backend mints with the workspace’s signing key, carrying the user’s distinct_id. Keystone uses it for the embedded preference centre and template editor. Every key is stored in environment variables, never in client code.
Node SDK and Hub REST — per-workspace credentials
Onhub.suprsend.com, using the workspace key and secret (backend SDK) or API key (REST). Keystone uses the Node SDK.
Management API — account Service Token
Onmanagement-api.suprsend.com, with the workspace as a path segment.