Next.js
The example app is a
complete Next.js integration: an inbox, a timeline, delegation, admin tools and a demo clock. The
code on this page comes from it. Run it with pnpm demo from a clone of the repository.
One instance per server process
Section titled “One instance per server process”Create Rungs once and share it. In development, Next.js reloads modules, so keep the instance on
globalThis:
// One instance per server process. Kept on globalThis so hot reload in `next dev` does not open// the PGlite database a second time.const state = globalThis as unknown as { rungsDemoDb?: Promise<PgClient>; demo?: Promise<Demo>;};
/** The server process's one database connection (or pool). */export function sharedDatabase(): Promise<PgClient> { state.rungsDemoDb ??= openDatabase().catch((error: unknown) => { state.rungsDemoDb = undefined; throw error; }); return state.rungsDemoDb;}
export function getDemo(): Promise<Demo> { state.demo ??= (async () => { const demo = await createDemo(await sharedDatabase()); const { seedIfEmpty } = await import("./seed"); await seedIfEmpty(demo); return demo; })().catch((error: unknown) => { state.demo = undefined; throw error; }); return state.demo;}From the example app:examples/nextjs-expense/rungs/demo.ts
/** One Rungs instance per schema. `clock` and `ids` are injectable so the seed can replay the past. */export function rungsFor( db: AppDb, inactive: Set<string>, clock: () => Date, ids: () => string = () => newId("r"),): Rungs { const rungs: Rungs = createRungs({ store: postgresStore(db, { schema: db.schema }), clock, ids, ...resolutionHooks(inactive), // After every command commits: tell the people involved. onEvent: async (event) => { const request = await rungs.get(event.requestId); if (request !== null) await notify(db, event, request); }, }); return rungs;}From the example app:examples/nextjs-expense/rungs/demo.ts
Rungs runs in the Node.js runtime only: it needs a database connection. Do not import it in Client Components or in the Edge runtime.
Lists in Server Components
Section titled “Lists in Server Components”Ask Rungs for the approval status and load your own records for the page in one query:
// The approval status comes from Rungs, newest first; the details from the app's own table.const page = await rungs.requests({ requesterId: user.id, limit: 20, ...(after === undefined ? {} : { after }),});const expenses = await expensesByIds( db, page.requests.map((r) => r.subject.id),);From the example app:examples/nextjs-expense/app/expenses/page.tsx
Commands in Server Actions
Section titled “Commands in Server Actions”Each Server Action runs one command. Take the actor from the session, never from form fields, and send back the version the form was rendered with, so that a stale form gets a conflict message instead of overwriting someone else’s decision:
export async function decideAction( requestId: string, version: number, _: ActionState, form: FormData,): Promise<ActionState> { const user = await requireUser(); const tooLong = commentTooLong(text(form, "comment")); if (tooLong !== undefined) return { error: tooLong }; const { rungs } = await requireDemo(); const input = { requestId, actorId: user.id, expectedVersion: version, ...comment(form) }; switch (text(form, "decision")) { case "approve": return attempt(() => rungs.approve(input), "Approved."); case "reject": return attempt(() => rungs.reject(input), "Rejected."); case "return": return attempt( () => rungs.return({ ...input, to: "requester" }), "Returned to the requester.", ); default: return { error: "Choose approve, reject or return." }; }}From the example app:examples/nextjs-expense/app/actions.ts
The example’s attempt helper turns a RungsError into a message for the form and revalidates
the page. Rungs does not check permissions such as who is an administrator; check them in the
action, as the example does with requireAdmin().
Time-based work
Section titled “Time-based work”Server Actions and pages do not run on a schedule. Run tick and event delivery in a separate
worker, a cron job or a scheduled route; see Running tick.