Presets
Start an app from a named preset or a code from /create, with every block it needs and the conventions it sets.
A preset is a block selection plus the conventions to install it with:
framework, platforms, casing, id type, scope, tenant name, access model, API
schema and integrations. Put a named preset or a code from
Create in your config, and
defineConfig expands it into blocks, with the blocks they require:
import { defineConfig } from "better-supabase/config";
export default defineConfig({
preset: "saas",
sql: { api: "api" },
blocks: {
comments: { options: { maxBodyLength: 5000 } },
tags: {},
},
});The keys you set win. A block you name joins the preset, and its entry is
merged over the preset's settings for that block. casing, sql.api and
integrations from the preset apply only when the config leaves them unset.
npx better-supabase sql sync then writes the files of every block the
preset expands to, and supabase db schema declarative sync turns them into
a migration (SQL modules).
A new project starts from a preset with
npx better-supabase init --preset saas, which writes the config, the module
files and lib/blocks.ts in one step. better-supabase apply --preset crm
applies one to a project you have, and better-supabase preset info shows
how your config differs from its preset (CLI).
Named presets
| Preset | Blocks |
|---|---|
minimal | profiles |
saas | organizations, invitations, teams, profiles, billing, entitlements, notifications, audit, api-keys, settings, onboarding, outbox |
crm | saas plus parties, pipelines, engagements, tags, comments, custom-fields, saved-views, search |
projects | saas plus tags, comments, attachments, state-machine, rank, reminders, time-tracking |
commerce | saas plus catalog, inventory, orders, promotions |
scheduling | saas plus calendar, bookings, reminders |
ai | saas plus ai-chat, ai-files, knowledge, memory, agents, ai-tasks |
everything | every block, for demos and tests |
Each one also installs what its blocks require (tenant, access,
updated-at, ...). The named presets use the default conventions.
tests/validation-presets renders each one, applies it to the local stack
as one migration, and typechecks the lib/blocks.ts it generates.
Apps built on a preset
These pages start from a preset and add the subject table, the options and
the event flow of one product. tests/validation-recipes installs each
config on the local stack and drives the flows end to end.
| App | Preset | Subject | Adds |
|---|---|---|---|
| CRM | crm | deal | rank, reminders, state-machine |
| Projects | projects | task | number-sequences, the state machine's workflow |
| Drive | saas | document | comments, tags, attachments, share-links, rank |
| Scheduling | scheduling | booking | custom-fields, share-links, state-machine |
| Commerce | commerce | order | imports, number-sequences, search, saved-views, metrics, approvals |
Codes and Create
Create builds a preset in the browser
and keeps it in the URL as /create?preset=<code>. The code also works as
preset in the config, with init --preset and with apply --preset, and
so does the whole URL. See
Codes and the resolver for the encoding and the
better-supabase/preset API.
The shared wiring
Three pieces connect the blocks in every recipe.
The subject is declared once in config.subjects. Comments, tags, custom
fields, share links and reminders all read it from the
subject registry, and rank reads its rank entry.
A state machine is named after its subject (deal, task, booking). The
machine name is the subjectType of its history rows and of
status.changed, so a status change reaches the same watchers as a comment.
A cron route hands the outbox to the watchers. watcherHandler reads the
actor from each outbox row, so whoever acts is not notified of their own
change:
import { watcherHandler } from "better-supabase/blocks/notifications";
const watchers = watcherHandler(notifications, {
types: {
"comment.created": "subject.commented",
"subject.tagged": "subject.tagged",
"status.changed": "subject.moved",
"share_link.created": "subject.shared",
"field_value.set": "subject.field_changed",
},
});
export const GET = outbox.relayRoute({
secret: process.env.CRON_SECRET,
consumers: { watchers },
});Users follow a row with notifications.subscribe({ subject, level }).
Watchers at level all get every mapped event. See
watchers for rules with activity,
recipients and summary.
Reminders deliver through the notifications block, the notifier port,
when the reminders block has a notification type. The reminder's owner and the subject's watchers get
one notification per occurrence:
const { comments, reminders, stateMachine } = createBlocks(
{ transport, service },
{
notifications: notificationsBlock.using((options) =>
createNotifications({ ...options, types }),
),
comments: commentsBlock,
reminders: remindersBlock.with({ notification: "reminder.due" }),
stateMachine: stateMachineBlock,
},
);reminders.job runs as the handler of the reminders.deliver queue
(delivery).
Last updated on