Bootstrap Endpoints

Postgres

The most common production setup: one Express (or any Node) route, a handful of concurrent queries, and a single JSON response that flux.bootstrap() reads in one network call per scope.

Prerequisites
  • A running Postgres database and a Node backend that can query it (pg, Prisma, Drizzle — any client works, examples here use pg directly)
  • Tables with an updated_at column or a single-state flux_changelog table if you plan to use realtime delta catch-up — see Step 4
  • Familiarity with api/bootstrap on the client side
1

A basic bootstrap route

The response shape is fixed: { ok, data, assets }. Keys inside data must exactly match the map object in your client-side flux.bootstrap() call.
server/routes/bootstrap.ts
1// server/routes/bootstrap.ts
2import express, { Router, Request, Response } from 'express'
3import { Pool } from 'pg'
4
5const router: Router = express.Router()
6const pool = new Pool({ connectionString: process.env.DATABASE_URL })
7
8router.get('/public', async (req: Request, res: Response) => {
9 try {
10 const { rows: services } = await pool.query(
11 'SELECT * FROM services ORDER BY sort_order ASC'
12 )
13 const { rows: team } = await pool.query(
14 'SELECT * FROM team_members ORDER BY name ASC'
15 )
16
17 res.json({
18 ok: true,
19 data: { services, team },
20 assets: [],
21 })
22 } catch (err: any) {
23 res.status(500).json({ ok: false, error: err.message })
24 }
25})
26
27export default router
2

One network call, not one query

Flux's single-packet guarantee is about the request the browser makes, not the number of queries your route runs. Bundle as many queries as you have registered channels, and run them concurrently.
typescript
1// Flux's contract is one network call, NOT one query. Nothing stops
2// this route from running several queries — one per registered
3// channel — as long as they're all bundled into the single JSON
4// response. This is the correct shape, not an N+1 problem:
5
6const [services, team, pricing] = await Promise.all([
7 pool.query('SELECT * FROM services ORDER BY sort_order ASC'),
8 pool.query('SELECT * FROM team_members ORDER BY name ASC'),
9 pool.query('SELECT * FROM pricing_tiers ORDER BY price ASC'),
10])
11
12res.json({
13 ok: true,
14 data: {
15 services: services.rows,
16 team: team.rows,
17 pricing: pricing.rows,
18 },
19 assets: [],
20})
21
22// Promise.all keeps these concurrent rather than sequential — the
23// client still only ever sees ONE request in the Network tab.
Promise.all, not sequential awaits

With three or more registered channels, sequential await calls add up fast on the client's perceived load time even though it's still technically one request. Promise.all keeps the queries concurrent server-side.

3

A 'user'-scope endpoint

A 'user'-scope bootstrap call is a separate endpoint, separate from the 'global' one — with its own leader lock, cooldown, and revalidation blueprint on the client (Section 4.13). Server-side, the only difference is that every query needs a WHERE user_id = ... clause, resolved from your own auth middleware, not from anything the client sends.
javascript
1// server/routes/bootstrap.js (excerpt) — a 'user'-scope endpoint
2router.get('/user', requireAuth, async (req, res) => {
3 const userId = req.user.id // set by your own auth middleware
4
5 try {
6 const { rows: dashboard } = await pool.query(
7 'SELECT * FROM dashboard_widgets WHERE user_id = $1',
8 [userId]
9 )
10 const { rows: billing } = await pool.query(
11 'SELECT * FROM billing_summary WHERE user_id = $1',
12 [userId]
13 )
14
15 res.json({
16 ok: true,
17 data: { dashboard, billing },
18 assets: [],
19 })
20 } catch (err) {
21 res.status(500).json({ ok: false, error: err.message })
22 }
23})
24
25// The client-side call this endpoint answers:
26//
27// flux.bootstrap({
28// endpoint: '/api/bootstrap/user',
29// scope: 'user',
30// userId: currentUser.id,
31// map: { dashboard: dashboardAdapter, billing: billingAdapter },
32// })
33//
34// Always parameterize by req.user.id from YOUR OWN auth middleware —
35// never trust a userId passed in the request body or query string.
36// Flux's userId field on BootstrapConfig is for client-side IDB
37// namespacing only; it carries no authority server-side.
Never trust a client-supplied userId server-side

Flux's BootstrapConfig.userId field exists purely for client-side IDB namespacing. Your endpoint must resolve the authenticated user independently — from a session cookie or verified token — and filter every query by that, never by a value read out of the request.

4

Delta queries for sync anchors

Separate from the bootstrap endpoint, your realtime adapter's delta catch-up path queries the single-state flux_changelog table. This handles row backfill, sparse field updates, and offline deletions when a channel reconnects with a non-zero SyncAnchorMap entry.
sql
1-- Server-side contract for SyncAnchorMap (Section 4.1):
2-- For each channel, query the single-state flux_changelog table
3-- instead of querying the live table directly. This ensures that
4-- offline deletions and merged sparse field updates are returned accurately.
5
6SELECT row_id, op, row_data, changed_at
7FROM flux_changelog
8WHERE table_name = $1 AND changed_at > to_timestamp($2 / 1000.0)
9ORDER BY changed_at ASC
10LIMIT 2000;
11
12-- $1 is the table name associated with the channel.
13-- $2 is the per-channel anchor value (unix ms) sent during reconnect or sync.
14-- Single-state dedup guarantees at most one row per entity in the result set.
5

Backing a handshake replay strategy

If any of your queue configs use replayStrategy: 'handshake', you need a separate revalidation endpoint — this is what revalidateFn calls, once per endpoint group, with the full manifest of queued entries.
javascript
1// server/routes/items-revalidate.js — backing a handshake QueueConfig
2// This is the Pass 1 bulk probe endpoint revalidateFn calls, NOT the
3// bootstrap endpoint. Section 4.6: called ONCE per endpoint group
4// with ALL queued entries' ids + clock-skew-corrected timestamps.
5router.post('/items/revalidate', async (req, res) => {
6 const { manifest } = req.body // [{ id, timestamp }, ...]
7 const ids = manifest.map((m) => m.id)
8
9 const { rows } = await pool.query(
10 'SELECT id, updated_at FROM items WHERE id = ANY($1)',
11 [ids]
12 )
13
14 const conflictedIds = []
15 const serverStates = {}
16
17 for (const entry of manifest) {
18 const serverRow = rows.find((r) => r.id === entry.id)
19 if (serverRow && serverRow.updated_at.getTime() > entry.timestamp) {
20 conflictedIds.push(entry.id)
21 serverStates[entry.id] = serverRow
22 }
23 }
24
25 res.json({ conflictedIds, serverStates })
26})
27
28// The matching client-side queueConfig:
29//
30// queueConfig: {
31// storeName: 'items_mutation_queue',
32// replayEndpoint: '/api/items',
33// replayStrategy: 'handshake',
34// revalidateFn: async (entries, manifest) => {
35// const res = await fetch('/api/items/revalidate', {
36// method: 'POST',
37// body: JSON.stringify({ manifest }),
38// })
39// return res.json()
40// },
41// }
6

What not to do

Two mistakes show up repeatedly in early Postgres integrations — both undermine the guarantees Flux is built to provide.
typescript
1// Don't do this — sequential queries with no bundling defeats the
2// entire point of a single-packet bootstrap:
3
4app.get('/api/services', ...) // ❌ separate endpoint
5app.get('/api/team', ...) // ❌ separate endpoint
6app.get('/api/pricing', ...) // ❌ separate endpoint
7// three client-side fetch() calls on mount — exactly what Flux's
8// bootstrap module exists to replace with one.
9
10// Also avoid: forgetting the WHERE user_id = $1 clause on a 'user'
11// scope endpoint. Flux's client-side scoping (Section 4.17) isolates
12// the CACHE per tenant — it has no way to stop your own endpoint
13// from leaking another user's row if the query itself doesn't filter
14// by the authenticated request's user id.

For MongoDB, see bootstrap/mongodb. For a framework-agnostic Node reference (including assets[] chunk warming for Next.js), see bootstrap/node. For any other backend language, see bootstrap/rest. For the client-side call this endpoint answers, see api/bootstrap.