Architecture
How Downcity is organized into Kernel, Capabilities, Interfaces, and Composition.
The most stable way to understand Downcity is not to start from a single API, but from its four layers:
Interfaces
-> products enter the Federation through client or operator tools
Composition
-> server / worker assemble @downcity/city, official services, and models
Kernel
-> City handles routing, auth, context, and hook scheduling
Capabilities
-> AIService and official services provide the actual behaviorTogether they create the runtime flow:
product UI / app / internal tool
-> User City / Admin City / City terminal
-> Federation
-> Service / AIService
-> Provider / DatabaseMany clients can share one self-deployed runtime. Product clients stay lightweight and focus on UX; the Federation centralizes auth, model routing, runtime env, hooks, and reusable capabilities.
1. Interfaces: who calls City
Downcity has two main external entry points:
- Product-side entry:
User CityandAdmin Cityfrom@downcity/city - Operator entry:
downcity
Neither one implements the server runtime itself. They both send requests into the same Federation.
2. Composition: who assembles City
Running Downcity is not just new City(...). There is also an assembly layer:
templates/nodeis the Node.js + SQLite developer startertemplates/edgeis the Cloudflare Workers + D1 developer starter- each city block owns its own City assembly logic for its runtime
This layer is responsible for assembling City + AIService + official services + models into a runnable instance.
3. Kernel: what City itself owns
When an HTTP request reaches City, City does the following:
- Refresh the runtime env view
- Validate the
user_tokenor admin key - Resolve identity,
city_id, anduser_id - Find the target Service and Action
- Build one shared
ctx - Run hooks and the Action
If the token is invalid, expired, or the city_id does not match, City returns 401 or 403 directly.
4. Capabilities: Service and Action
Each Service is a group of Actions. An Action is the first-class capability unit inside a Service.
const translate = new Service({ id: "translate" });
translate.action("zh2en", async (ctx) => {
// ctx.input = { text: "你好" }
// ctx.user = { user_id: "user_1" }
return { translated: await api.translate(ctx.input.text) };
});City automatically creates a route for every Action:
POST /v1/translate/zh2en -> translate.action("zh2en")City itself does not care what “translation” or “payment” means as business concepts. It only routes the request to the right Action.
5. Two AIService pathways
SDK pathway
Used by User City:
User City.ai.text({ prompt: "hello", model: "deepseek-v4-flash" })
-> POST /v1/ai/text
-> Provider text action(ctx)
-> generateText / streamText (ai-sdk)
-> UIMessage / UIMessageStreamOpenAI-compatible pathway
Used by downcity agent, the OpenAI SDK, curl, and other third-party tools:
POST /v1/ai/chat/completions
{ model: "deepseek-v4-flash", messages: [...], stream: true }
-> Provider openai action(ctx) or automatic passthrough
-> upstream API raw Response (SSE or JSON)The two pathways are fully decoupled and operate independently.
6. Automatic passthrough
When a Provider has baseURL + envKey but no openai action, AIService generates a passthrough action automatically. The OpenAI request body from the third-party tool is forwarded upstream as-is, and the upstream Response is returned as-is.
No adapter code is required.
7. Three hook layers
Every Action has its own hooks. Hook execution runs from outer to inner layers:
global.before
-> service.before <- shared by all actions in the service
-> action.before <- only this action
-> action.run() <- core logic
-> action.after <- billing, usage records
-> service.after <- aggregated metrics
-> global.after <- global monitoring// Action-level hook
const zh2en = svc.action("zh2en", fn);
zh2en.before(checkBalance).after(deductFee);
// Service-level hook shared by all actions
svc.hook.after(async (ctx) => {
console.log(`${ctx.user.id} used ${ctx.service.id}.${ctx.action.id}`);
});8. The one-line model
You can remember Downcity like this:
Interfaces handle entry
Composition handles assembly
Kernel handles runtime rules
Capabilities handle actual behavior