Federation
The role of the Federation instance, runtime and serve inside @downcity/city.
@downcity/city's Federation is the long-running server container of Downcity. It is where a Database Adapter, services, auth, and the shared /v1/* route space come together. Product-side access uses the City client; see Federation and City for the distinction.
What this concept means
A simple way to think about Federation is:
one reusable AI infrastructure runtime process that many products can keep calling over time.
That process owns:
- the Database Adapter
- the service registry
- service mounting
- the HTTP route surface
- city, token, and env infrastructure
When this page matters
- you are about to run a real Federation for the first time
- you want to understand why
Federationonly needs a Database Adapter - you want clarity on
fetch(), andserve() - you want to mount Federation into Node, Workers, or another HTTP host
Minimal runnable example
import { AIService, Federation } from "@downcity/federation";
import { Database } from "@downcity/database-sqlite";
import { serve } from "@hono/node-server";
const database = new Database({ filename: "./.base/downcity.sqlite" });
const base = new Federation({ database });
const ai = new AIService();
ai.use({
id: "local-echo",
name: "Local Echo",
actions: {
text: async (ctx) => ({
id: crypto.randomUUID(),
role: "assistant",
parts: [
{
type: "text",
text: String(ctx.input.prompt ?? ""),
state: "done",
},
],
}),
},
});
base.use(ai);
await base.health();
serve({
fetch: (request) => base.fetch(request),
port: 43127,
hostname: "127.0.0.1",
});The important ideas in that example are:
databasedecides which Database Adapter Federation uses.base.use(...)decides which capabilities Federation exposes.base.fetch()decides how Federation enters your HTTP layer.
Scenario 1: Federation is the main service
If you want Federation itself to be the main HTTP entry, serve() is the clearest mental model:
import { serve } from "@hono/node-server";
await base.health();
serve({ fetch: (request) => base.fetch(request), port: 3001, hostname: "127.0.0.1" });Scenario 2: Federation is mounted into your existing server
If you already have a server framework, handing requests into Federation is often the cleaner pattern:
export default {
async fetch(request: Request) {
return base.fetch(request);
},
};Concurrent Cloudflare Worker cold starts
A single Worker deployment can start multiple isolates at the same time. Each isolate has independent memory while sharing the same D1 database, so a module-level Promise only prevents duplicate initialization inside one isolate and is not a global lock.
Federation atomically initializes its system env and Ed25519 user token signing key in the shared database, with a unique constraint that allows only one active signing key. If an older SDK left multiple active keys behind, startup keeps the earliest key and changes the others to retired. Retired keys remain in JWKS so existing tokens can still be verified.
Scenario 3: Local HTTP
When Federation only needs to be called by trusted processes on the same machine, expose its standard fetch handler through your local HTTP server:
import { Federation } from "@downcity/federation";
import { serve } from "@hono/node-server";
const base = new Federation({ database });
serve({
fetch: (request) => base.fetch(request),
hostname: "127.0.0.1",
port: 15315,
});
const federation_url = "http://127.0.0.1:15315";Then point City at that HTTP URL:
import { FederationAdmin } from "@downcity/federation/legacy";
const admin = new FederationAdmin({
base_url: federation_url,
credential: administrator_session_token,
});
await admin.listServices();Local HTTP uses the same product-side model as remote HTTP: FederationAdmin uses an administrator session token, public actions can be called as guest, and authenticated user actions require a normal user_token.
If AccountsService is configured with local_login: true, /v1/accounts/providers only returns the local login method. Call accounts.login/start with provider: "local" and bureau_id to receive a login_id, then read the normal user_token from accounts.login/result and recreate or update User City with user_token (which contains bureau_id).
Why Federation only takes a Database Adapter
@downcity/city owns the logic that should stay stable across runtimes:
- service lifecycle
- action routes
- token auth
- city / env / store infrastructure
The host runtime only needs to create the appropriate adapter:
- Node.js can use
@downcity/database-sqliteor@downcity/database-postgresql - Cloudflare Workers can use
@downcity/database-d1 - Federation writes required runtime env into the built-in
envtable on boot
Default Storage
When a Federation needs to move runtime files into first-party storage, register a default storage backend:
import { Federation, R2Storage } from "@downcity/federation";
const base = new Federation({ database });
base.storage(R2Storage({
bucket: env.DOWNCITY_STORAGE,
public_url_prefix: env.DOWNCITY_STORAGE_PUBLIC_URL_PREFIX,
}));Services can then access it through ctx.storage. Built-in AI image jobs use this storage after image_fetch succeeds to move remote file part URLs into your own bucket. If storage fails, the original upstream URL is kept and the image job still succeeds.
Common API surface
new Federation({ database })base.use(...)base.storage(...)base.fetch(request)base.health()base.table(name)
Common misunderstandings
Federation is the runtime, not the whole SDK
Federation is the long-running server runtime. Product-side calling uses the City client; see Client SDK.
Federation is not only an HTTP proxy
It also owns built-in tables, token and env infrastructure, service data layers, and hook lifecycle.
Federation is not one provider
Providers, models, services, and custom services are capabilities mounted into Federation, not the Federation itself.
Read next
- For what actually gets mounted into Federation, read Service and Action
- For hooks and extension points, read Hooks and Service Mounting
- For persistence and tables, read Store and Table
- For lower-level method details, read Federation API