Bootstrap Endpoints

REST (any backend)

The bootstrap contract is just an HTTP response shape — nothing about it requires Node. This page covers wiring it up from Python, Go, and Ruby, plus the two things a genuinely cross-origin backend needs to get right: CORS and the Date header.

Prerequisites
  • Any backend that can serve JSON over HTTP — examples here use Flask/FastAPI, Go, and Rails
  • For Node-specific patterns (including Next.js's assets[] chunk warming), see bootstrap/node instead
  • Familiarity with api/bootstrap on the client side
1

The contract, independent of language

Every bootstrap endpoint answers to exactly this response shape, regardless of what produced it. This is the entire spec — everything else on this page is one backend's way of producing it.
json
1// The bootstrap API contract — language-agnostic. Flux's client-side
2// bootstrap module only cares about this HTTP response shape, never
3// about what produced it:
4
5// GET (or POST) https://api.yourapp.com/bootstrap/public
6// Content-Type: application/json
7
8{
9 "ok": true,
10 "data": {
11 "services": [ /* ... */ ],
12 "team": [ /* ... */ ]
13 },
14 "assets": [] // always [] outside Next.js — see Step 4
15}
16
17// Keys inside "data" must exactly match the map object in your
18// client-side flux.bootstrap() call. Nothing else about this
19// contract assumes Node, Postgres, or any particular stack.
2

Python — Flask and FastAPI

Flask shown synchronously, FastAPI shown with concurrent queries via asyncio.gather — the same one-network-call-multiple-queries pattern from the Postgres and MongoDB guides applies here too.
python
1# PythonFlask
2from flask import Flask, jsonify
3from services import get_services, get_team
4
5app = Flask(__name__)
6
7@app.route('/api/bootstrap/public', methods=['GET'])
8def bootstrap_public():
9 try:
10 return jsonify({
11 'ok': True,
12 'data': {
13 'services': get_services(),
14 'team': get_team(),
15 },
16 'assets': [],
17 })
18 except Exception as err:
19 return jsonify({'ok': False, 'error': str(err)}), 500
20
21# FastAPIthe async equivalent, same response shape
22from fastapi import FastAPI
23import asyncio
24
25app = FastAPI()
26
27@app.get('/api/bootstrap/public')
28async def bootstrap_public():
29 services, team = await asyncio.gather(get_services(), get_team())
30 return {
31 'ok': True,
32 'data': {'services': services, 'team': team},
33 'assets': [],
34 }
3

Go and Ruby on Rails

The response struct/hash differs by language, but the three top-level keys — ok, data, assets — stay fixed.
go
1// Go — net/http, no framework
2package main
3
4import (
5 "encoding/json"
6 "net/http"
7)
8
9type BootstrapResponse struct {
10 OK bool `json:"ok"`
11 Data map[string]interface{} `json:"data"`
12 Assets []string `json:"assets"`
13}
14
15func bootstrapPublicHandler(w http.ResponseWriter, r *http.Request) {
16 services, err := getServices()
17 if err != nil {
18 w.WriteHeader(http.StatusInternalServerError)
19 json.NewEncoder(w).Encode(map[string]interface{}{
20 "ok": false, "error": err.Error(),
21 })
22 return
23 }
24 team, err := getTeam()
25 if err != nil {
26 w.WriteHeader(http.StatusInternalServerError)
27 json.NewEncoder(w).Encode(map[string]interface{}{
28 "ok": false, "error": err.Error(),
29 })
30 return
31 }
32
33 w.Header().Set("Content-Type", "application/json")
34 json.NewEncoder(w).Encode(BootstrapResponse{
35 OK: true,
36 Data: map[string]interface{}{
37 "services": services,
38 "team": team,
39 },
40 Assets: []string{},
41 })
42}
43
44func main() {
45 http.HandleFunc("/api/bootstrap/public", bootstrapPublicHandler)
46 http.ListenAndServe(":8080", nil)
47}
ruby
1# Ruby on Railsapp/controllers/bootstrap_controller.rb
2class BootstrapController < ApplicationController
3 def public_data
4 render json: {
5 ok: true,
6 data: {
7 services: Service.order(:sort_order).as_json,
8 team: TeamMember.order(:name).as_json,
9 },
10 assets: [],
11 }
12 rescue => e
13 render json: { ok: false, error: e.message }, status: 500
14 end
15end
16
17# config/routes.rb
18# get '/api/bootstrap/public', to: 'bootstrap#public_data'
4

assets[] is always empty here

Dynamic route chunk warming (Section 4.13) is a Next.js-specific mechanism with no equivalent build artifact outside that framework — omit this field or return it empty.
json
1// assets[] is Next.js-specific — see Section 4.13 and bootstrap/node.
2// It's extracted server-side from .next/app-build-manifest.json to warm
3// dynamic route chunks in the service worker. A non-Node backend has
4// no equivalent build artifact to read at request time, so this
5// field is simply omitted or returned as an empty array:
6
7{
8 "ok": true,
9 "data": { "services": [], "team": [] },
10 "assets": []
11}
12
13// If you're serving a Vite/React SPA from this same REST backend
14// (see getting-started/react), this is exactly the value it expects
15// too — see that guide's bootstrap route step for the client-side
16// half of the story.
5

Don't strip the Date header

flux.bootstrap() reads this header to compute clockSkewMs, which feeds directly into handshake conflict detection — not just a display value.
typescript
1// flux.bootstrap() reads the response's Date header to compute
2// clockSkewMs (Section 4.13) — this correction feeds directly into
3// replayQueue()'s handshake timestamp comparisons, so a backend that
4// suppresses or overrides this header breaks conflict detection
5// accuracy, not just cosmetic clock display.
6
7// serverTime = new Date(response.headers.get('Date')).getTime()
8// latency = (Date.now() - requestStartTime) / 2
9// clockSkewMs = serverTime - (requestStartTime + latency)
10
11// Every mainstream HTTP server (Flask's Werkzeug, FastAPI/Uvicorn,
12// Go's net/http, Rails/Puma) sends a valid Date header automatically
13// — this is a "don't strip it" note, not something to add yourself.
14// Only relevant if you're behind a reverse proxy or CDN that's
15// configured to rewrite response headers.
6

CORS — the thing this page needs that the others don't

A REST backend in a different language is commonly served from a different origin than the frontend entirely, unlike the Node examples elsewhere in these docs. This is the first thing to check if flux.bootstrap() silently never resolves.
typescript
1// Unlike the Node examples elsewhere in these docs — which typically
2// run same-origin behind Next.js API routes or a proxied dev server —
3// a REST backend in a different language is very often served from a
4// genuinely different origin than the frontend. CORS is the first
5// thing to check if flux.bootstrap() silently never resolves.
6
7// Flask
8from flask_cors import CORS
9CORS(app, resources={r"/api/*": {"origins": "https://yourapp.com"}})
10
11// FastAPI
12from fastapi.middleware.cors import CORSMiddleware
13app.add_middleware(
14 CORSMiddleware,
15 allow_origins=["https://yourapp.com"],
16 allow_methods=["GET", "POST"],
17)
18
19// Go — net/http, manual header (or use a library like rs/cors)
20w.Header().Set("Access-Control-Allow-Origin", "https://yourapp.com")
21
22// Rails — config/initializers/cors.rb
23Rails.application.config.middleware.insert_before 0, Rack::Cors do
24 allow do
25 origins 'https://yourapp.com'
26 resource '/api/*', headers: :any, methods: [:get, :post]
27 end
28end
29
30// The Date response header (Step 5, clock skew) must also be exposed
31// if you set Access-Control-Expose-Headers explicitly anywhere — most
32// servers send Date by default and most CORS setups don't need to
33// list it, but double-check if you've locked down exposed headers.
A CORS block looks identical to a network failure

Per Section 4.13's failure handling, a blocked CORS request falls into the same "serve existing IDB data, never roll back" path as a genuine network error — there's no console signal from Flux itself distinguishing the two. Check the browser's Network tab directly if bootstrap seems to silently do nothing.

7

Extending to a 'user'-scope endpoint

Same shape, same rule as every other backend in this section: resolve the authenticated identity server-side, never trust a client-supplied id.
python
1# A 'user'-scope endpoint follows the exact same shapethe only
2# addition is resolving the authenticated identity server-side and
3# filtering your data-layer calls by it, regardless of language:
4
5@app.route('/api/bootstrap/user', methods=['GET'])
6@require_auth
7def bootstrap_user():
8 user_id = g.current_user.id # from YOUR OWN auth middleware
9 # never trust a client-supplied id
10
11 return jsonify({
12 'ok': True,
13 'data': {
14 'dashboard': get_dashboard(user_id),
15 'billing': get_billing(user_id),
16 },
17 'assets': [],
18 })
19
20# The client-side call this endpoint answers:
21#
22# flux.bootstrap({
23# endpoint: 'https://api.yourapp.com/bootstrap/user',
24# scope: 'user',
25# userId: currentUser.id,
26# map: { dashboard: dashboardAdapter, billing: billingAdapter },
27# })
8

Status codes and how the client reacts

Identical across every backend language — worth confirming your framework's error handler actually returns a real 5xx rather than a 200 with an error body buried in JSON.
typescript
1// How the client treats each response, from Section 4.13 — same
2// across every backend language:
3
4// 2xx + { ok: true, data, assets } -> dispatched normally
5// 5xx -> cooldown reset to 0, so the
6// next online event retries
7// immediately; existing IDB
8// data keeps serving meanwhile
9// any other failure (network error,
10// 4xx, malformed JSON, CORS block) -> existing IDB data keeps
11// serving, NEVER rolled back
12
13// Return a genuine 5xx for transient backend failures so the
14// retry-immediately path kicks in — reserve 4xx for real client
15// errors like a missing/invalid auth token on a 'user' scope call.

For Node-specific patterns, see bootstrap/node. For Postgres or MongoDB query patterns specifically, see bootstrap/postgres or bootstrap/mongodb. For the client-side call this endpoint answers, see api/bootstrap.