App shapes

Publish a FastAPI app

FastAPI works like any async Python app on Ellivate. The auth middleware is injected at publish time; your routes see a per-viewer identity automatically. No sign-in routes, no database drivers.

A minimal FastAPI app

Save as main.py:

from fastapi import FastAPI
import ellivate

app = FastAPI()

@app.get("/")
async def home():
    viewer = ellivate.viewer()
    items = ellivate.kv.get("shared:items") or []
    return {
        "welcome": viewer.username if viewer else "friend",
        "items": items,
    }

@app.post("/items")
async def add(item: str):
    current = ellivate.kv.get("shared:items") or []
    current.append(item)
    ellivate.kv.set("shared:items", current)
    return {"ok": True, "count": len(current)}

And a requirements.txt:

fastapi>=0.110
uvicorn>=0.27

That's a deployable FastAPI app. Run ellivate publish.

What the pipeline does

1. Writes ellivate_client.py

Dropped at project root. Import with import ellivate. No pip install — it's rewritten fresh on every publish so it stays in sync with the backend.

2. Writes ellivate_auth.py and wires middleware

The translator detects app = FastAPI() in your entry file and inserts init_ellivate_auth(app) right after. That call:

  • Adds a /ellivate-handoff route so the shell can set the viewer cookie.
  • Adds a Starlette middleware that verifies the cookie on every other request. Missing cookie → 401. Valid cookie → the request continues.
  • Stashes the viewer's identity on a contextvars.ContextVar the SDK reads on every KV call. That's how your ellivate.kv.get("personal:...") calls know whose personal data to return.

3. Generates a start command

FastAPI doesn't self-host; Ellivate writes a Procfile-equivalent that runs uvicorn main:app --host 0.0.0.0 --port $PORT. Binding to 0.0.0.0 is required for the container networking — localhost won't be reachable.

Per-viewer scopes in action

Each viewer gets their own personal:-scoped data. For example, a per-viewer draft:

@app.post("/drafts")
async def save_draft(body: str):
    # "personal:" prefix means each viewer has their own draft
    ellivate.kv.set("personal:draft", body)
    return {"saved": True}

@app.get("/drafts")
async def get_draft():
    return {"draft": ellivate.kv.get("personal:draft") or ""}

Two users hitting the same endpoint see different drafts. Ellivate handles the routing server-side based on the viewer cookie.

JSON APIs for a separate frontend

If you're shipping a FastAPI backend that a separate Next.js / React client will call, the client needs to forward the viewer token. Easiest approach: serve the client from the same Ellivate app (a static/ directory), or use window.ELLIVATE_VIEWER_TOKEN in the browser and add it to fetch headers:

// Client
const res = await fetch("/api/items", {
  headers: {
    "X-Ellivate-Viewer-Token":
      (window as any).ELLIVATE_VIEWER_TOKEN ?? "",
  },
});

Things to avoid

  • SQLAlchemy / databases / tortoise-orm for app state. Use ellivate.kv. If you genuinely need relational queries, Ellivate will flag it at publish time and ask you to upgrade — we don't ship a sqlite into a container that gets wiped on redeploy.
  • FastAPI-Users, fastapi-login, OAuth flows. Identity is the shell's job.
  • @app.on_event("startup") that calls KV. At startup there's no request, no viewer — the SDK falls back to server lane (app-wide data). If you really mean app-wide, that's fine; if you meant per-viewer, do it inside a route instead.

What's next