Skip to content

Self-hosting (sovereign deployment)

Futuros is designed to be redeployed in-region. This is Pillar 2 (the Sovereign Model): a LATAM government or multilateral can run the whole platform — the atlas, the baked corpus, the public API, and the AI assistant — on infrastructure inside its own jurisdiction, pointing inference at an in-region open-weight gateway so the corpus and every user query stay on sovereign soil.

Because the platform is a static SPA + baked JSON corpus + a small set of serverless functions, self-hosting comes in two tiers.

Two tiers, one repo

The repo's Dockerfile + Caddyfile produce a static deployment: the SPA, the baked public/data/ corpus, the Public API v1, and the embeds. That container (Caddy serving dist/) does not run the api/ serverless functions — so the chat, MCP server, data-trust contribute/revoke, consensus, and alerts endpoints are not served by it. To run those interactive services you need a host that executes the api/* Node functions (Vercel, or any Node/functions runtime), plus the env below.

Tier 1 — static sovereign mirror (Docker + Caddy)

The included multi-stage Dockerfile builds with Bun and ships a tiny Caddy image serving the static build:

dockerfile
FROM oven/bun:1.3 AS build
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile
COPY . .
ARG VITE_GATE_PASSWORD                 # baked at build time (default: Futuros42)
ENV VITE_GATE_PASSWORD=${VITE_GATE_PASSWORD}
RUN bun run build

FROM caddy:2-alpine
COPY --from=build /app/dist /srv
COPY Caddyfile /etc/caddy/Caddyfile
EXPOSE 8080
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

Build and run:

bash
docker build -t futuros --build-arg VITE_GATE_PASSWORD=your-gate-pass .
docker run -p 8080:8080 -e PORT=8080 futuros
# → http://localhost:8080

The Caddyfile listens on :{$PORT:8080}, gzips/zstd-compresses, sets the same security headers Vercel does, caches hashed /assets/* immutably and the /data/* corpus briefly, serves the marketing landing at /, and does SPA fallback (try_files {path} /index.html) so TanStack Router routes resolve. railway.json wires the same Dockerfile for Railway (builder DOCKERFILE, health check /intro).

This tier is a complete, offline-capable data-sovereignty mirror: the full atlas UI and the entire cited corpus, hosted wherever you run the container. Anything that needs a server (below) degrades honestly — the app stays usable, the interactive features announce they are not configured rather than faking a result.

Tier 2 — full platform (serverless functions + sovereign inference)

The interactive surfaces live in api/* as Node serverless functions (chat, mcp, contribute, revoke, consensus, alerts-subscribe, alerts-digest). Deploy them on a runtime that executes them (Vercel is the reference target) and set the env vars below. The static assets and corpus are the same build as Tier 1.

Environment variables

Grouped by feature. Everything is optional — each unset key degrades a specific feature honestly (see the table further down). Client keys are VITE_-prefixed and baked into the build; the rest are server-side only.

Note: the repo's env.example documents only the map, analytics, and chat/ retrieval keys. The trust-ledger (Upstash), alerts (Mailgun / webhooks), and Perplexity keys below are read by the code but are not in env.example — the authoritative list is here.

Build / client (baked, VITE_-prefixed):

VarFeatureWithout it
VITE_MAPBOX_TOKENMapbox basemap on the atlasMapLibre fallback renders boundaries on a dark canvas
VITE_GATE_PASSWORDPassword gate on the SPA (/atlas)Falls back to in-source default Futuros42
VITE_POSTHOG_KEYPostHog product analytics (prod domains only)No-op
VITE_POSTHOG_HOSTPostHog region overrideDefaults to US Cloud

AI assistant — inference provider (server-side; first match wins):

Var(s)Feature
SOVEREIGN_INFERENCE_URL + SOVEREIGN_INFERENCE_KEYPillar 2 sovereign path. Points the same Anthropic-Messages agent loop at an in-region / self-hosted open-weight gateway (LiteLLM, or vLLM/TGI behind a Messages-compatible shim, serving Llama/Qwen/DeepSeek/Mistral). When both are set they win over OpenRouter/Anthropic and the chat shows a "sovereign mode" badge — the query + corpus never leave the host.
SOVEREIGN_INFERENCE_MODELYour gateway's served-model-name (default local-model)
SOVEREIGN_INFERENCE_LABELUI label for the sovereign provider (default Soberano)
OPENROUTER_API_KEYManaged fallback: routes via OpenRouter's Anthropic-native endpoint (default model anthropic/claude-sonnet-4.6)
ANTHROPIC_API_KEYCalls api.anthropic.com directly (default claude-sonnet-4-6)
CHAT_MODEL / CHAT_FOLLOWUP_MODELOverride answer / follow-up model ids for the active provider

Provide one provider. Selection order is sovereign → OpenRouter → Anthropic; with none set, the chat emits a not_configured event and declines rather than erroring.

AI assistant — retrieval & external web (server-side):

Var(s)FeatureWithout it
VOYAGE_API_KEY (+ CHAT_EMBED_MODEL, CHAT_RERANK_MODEL)Semantic search_corpus over the baked index + cross-encoder rerankDegrades to keyword/BM25 search (no hard failure)
EXA_API_KEYChat's last-resort web fallback (web_search/fetch_url) + pulse ingestionChat answers from the corpus only and declines out-of-corpus questions
PERPLEXITY_API_KEY (+ PERPLEXITY_MODEL)perplexity_search live web synthesis (chat + MCP)Tool returns a "not configured" note; answers stay corpus-only

Data Trust (Pillar 1) & consensus — Upstash Redis (server-side):

Var(s)FeatureWithout it
UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKENAppend-only trust ledger (/contribuir receipts, /api/revoke) and consensus vote store (/api/consensus)Returns a valid receipt with persisted:false; consensus degrades to local-only — never fakes persistence

The Vercel KV aliases KV_REST_API_URL / KV_REST_API_TOKEN are also accepted.

Alerts & email (server-side):

Var(s)FeatureWithout it
MAILGUN_API_KEY + MAILGUN_DOMAIN + MAILGUN_FROMEmail delivery for /api/alerts-subscribe confirmations and Data-Trust contribution/revoke notices, via Mailgun over fetchalerts-subscribe returns a mailto: fallback instead of pretending to subscribe
MAILGUN_NOTIFY / ALERTS_NOTIFY_EMAILInternal copy of subscribe/contribution noticesNo internal copy
ALERTS_WEBHOOKSThe weekly digest cron (/api/alerts-digest, Mon 13:00 UTC) dispatches the global anomaly digest to these comma-separated webhook URLsDigest dispatches to nobody
CRON_SECRETAuthenticates the cron trigger to /api/alerts-digestEndpoint unguarded

The weekly alerts digest is webhook-based (ALERTS_WEBHOOKS), not email — Mailgun handles the subscribe confirmation and Data-Trust notices. Both exist; don't conflate them.

Ingestion (build-time scripts only — not needed to serve the app): the refresh pipeline reads keys such as OPENAQ_API_KEY, CLOUDFLARE_RADAR_TOKEN, YOUTUBE_API_KEY, REDDIT_CLIENT_ID / REDDIT_CLIENT_SECRET, SERPAPI_KEY, plus the EXA_/PERPLEXITY_/GDELT_ tuning vars. These matter only when you re-run ingestion (below), never to serve an already-baked corpus.

The corpus is static, baked at build time

There is no database in the serving path. The entire corpus under public/data/ is baked at build time and shipped as static files (the same files the Public API v1 exposes). Serving Futuros means serving those files — which is why Tier 1 (Caddy over dist/) is a complete data mirror.

To refresh the data you re-run the ingestion/bake scripts and rebuild:

bash
bun run ingest      # multi-source ingest → parameter-cache + citations + signals
bun run bake:all    # narratives, scores, indices, api, … → public/data/**
bun run build       # tsc + vite; prebuild provenance gates must pass

The prebuild step runs provenance gates (traceability, source deep-links, citations, scores, catalog) that fail the build if a figure lost its source — so a self-hosted rebuild preserves the honesty contract by construction. A sovereign operator can run this pipeline entirely in-region: baked corpus in-region, inference in-region, nothing leaves the host.

  1. Stand up an in-region open-weight gateway (LiteLLM in front of vLLM/TGI serving Llama/Qwen/DeepSeek/Mistral) that speaks the Anthropic Messages API.
  2. Deploy the SPA + baked corpus (Tier 1 container) and the api/* functions on in-region infrastructure.
  3. Set SOVEREIGN_INFERENCE_URL + SOVEREIGN_INFERENCE_KEY (+ _MODEL, _LABEL). The chat badges "sovereign mode"; corpus + queries never leave your jurisdiction.
  4. Add UPSTASH_REDIS_REST_* only if you want the trust ledger / consensus persisted; otherwise those features degrade honestly.

Cada cifra con su fuente — la trazabilidad es el contrato.