Examples
Runnable apps for every integration, built against a local stack.
Each example in apps/examples
was set up with better-supabase init, generates its types from the same
schema, and runs against the local stack from supabase/config.toml.
| Example | Shows |
|---|---|
nextjs | A multi-tenant SaaS app on Cache Components, with every page proved instant by instant() tests |
hono-api | createHono, bs.resource with a list query, a custom route, and an admin route behind bs.require |
express | Express 5 on better-supabase/node: toExpress, a role guard, problemErrorHandler, and a smoke test against the local stack |
orpc-api | An oRPC router with valibot inputs, bs.authed with roles, and a contract-first OpenAPI server on Hono |
expo-powersync | An Expo Router app: web reads in server loaders; iOS and Android read PowerSync with useWatch, upload with createUploadConnector, sign in with a magic link or OAuth, and register for push |
edge | An Edge Function with bs.routes, typed route params and role guards |
mcp | An MCP server as an Edge Function, exposing tables as tools, with requiredRoles |
eve | An eve agent on Next.js with the Supabase World, sign-in, Vault connections, memory and the inbox channel |
vite-react | An SPA with createClient, useSignIn, useLiveQuery, debounced search and an optimistic create |
Run them
supabase start
pnpm build:packages
pnpm --filter @better-supabase/example-nextjs devThe examples use the stack from supabase/config.toml (API on port 55421).
To use another stack, set SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and
SUPABASE_SECRET_KEY.
The Next.js SaaS app
apps/examples/nextjs is a generic SaaS app on Next.js Cache Components,
shadcn/ui (Base UI), next-intl and the local stack. Every page runs on the
real Auth server and database; nothing is mocked. Sign in with one of the
seeded users from supabase/seed.sql (password password123):
| User | Role |
|---|---|
admin@acme.test | Owner of Acme, member of Globex |
member@acme.test | Member of Acme |
owner@globex.test | Owner of Globex |
The features and the better-supabase parts they use:
| Feature | Uses |
|---|---|
| Sign-in, sign-up, password reset, MFA | bs.proxy with protect, the browser client, the Auth server and Mailpit |
| Organizations, switcher, invitations | the organizations and invitations SQL modules with better-supabase/blocks/organizations |
| Customers list and detail | defineListQuery with facets, bs.cached() in "use cache: private", comments, logo upload |
| Notifications | the notifications module with better-supabase/blocks/notifications, a live unread badge |
| Workflows | better-supabase/blocks/workflows and the Supabase World: a live runs list, run detail with cancel, an approval with authorizeHook, schedules |
| Workflow builder | better-supabase/blocks/workflow-builder on @xyflow/react: a node palette from the step library, a live run overlay, approvals on the canvas, trigger and credential drawers, publish with a version diff |
| Members, roles and permissions | the access module in custom mode, can() in policies and the UI |
| Billing and plan features | the entitlements and usage modules |
| API keys and audit log | the api-keys and audit modules, with a CSV export |
| Profile, settings, onboarding | the profiles, settings and onboarding modules |
| Announcements and a beta page | the announcements and flags modules |
The routes are localized under /en and /nl with next/root-params, and
the translations are .po files read directly by next-intl. The theme
(light, dark or system) is applied before the first paint.
pnpm --filter @better-supabase/example-nextjs test:e2e resets the stack,
builds the app with the testing API and runs the Playwright specs in
apps/examples/nextjs/e2e. They click every menu page for each user in both
languages under instant(), and walk through sign-in, the organization
switch, the language and theme switches and an invitation.
Real-world ports
tests/validation-crm and tests/validation-request-context rewrite code
from two production apps on better-supabase:
- CRM customers service: the customer list, which has status,
tag, assignee and nested contact search filters plus four includes, becomes
one
paginatecall that sends a single request. It is generated from and typechecked against a 263-table schema. - CRM latency ports, each asserting its request budget with
db.$stats():- The customers overview is a
defineListQuerywith facet counts and per-row aggregates: 2 calls in 1 wave. - The app-chrome badges (unread notifications, open tasks, pending approvals) are one read set: 1 call.
- The customer portal is scoped by
tenant({ claim: 'customer_id' })on claims validated bybetterSupabase.claims(): 2 calls in 1 wave.
- The customers overview is a
- An app Supabase package: env validation, request headers, a user-auth guard, a cron guard with a named secret key, avatar storage and the browser client. Its request-context tests run against the local stack.
Last updated on