Admin City
How trusted environments manage cities, issue user tokens, and maintain runtime env.
Admin City should only run in trusted environments.
Typical cases:
- your own city backend
- local admin scripts
- internal tools
- CI or operations scripts
Do not expose admin_secret_key to browsers, public frontends, or uncontrolled clients.
What it owns in the full system
The most accurate mental model is: Admin City is the trusted-side bridge into City management.
It usually owns three things:
- managing
city - issuing
user_token - maintaining runtime env
- running trusted-side balance / redeem_code administration
If User City owns "how a user-context city call reaches City," then Admin City owns "how the trusted side prepares that call environment."
Minimal example
import { City } from "@downcity/city";
const admin = new City({
role: "admin",
federation_url: "https://base.example.com",
city_id: "city_demo",
admin_secret_key: process.env.DOWNCITY_FEDERATION_ADMIN_SECRET_KEY,
});If admin_secret_key is omitted, the SDK tries to read process.env.DOWNCITY_FEDERATION_ADMIN_SECRET_KEY.
Typical call chain
/v1/cities/*, /v1/cities/tokens/apply, /v1/env/*, /v1/ai/models, and /v1/base/instruction.admin_secret_key, then executes city, token, and env management actions.tokens.apply()
This is the most common trusted-side action: issue a user_token for a user under the target city.
city_id is automatically injected from the City constructor. You only need to pass user_id.
const issued = await admin.tokens.apply({
user_id: "user_123",
metadata: {
plan: "pro",
org_id: "org_001",
},
ttl: "7d",
});The response includes:
user_tokencity_iduser_idexpires_at
ttl supports:
30m1h7d- raw seconds
Recommended login flow
router.post("/login", async (c) => {
const user_id = await login(c);
const issued = await admin.tokens.apply({
user_id,
ttl: "7d",
});
return c.json({
city_id: issued.city_id,
user_token: issued.user_token,
});
});That means:
- your backend handles user login first
- the trusted backend asks City for
user_token - it returns
city_id + user_tokento the client - the client calls City through
User City
Relationship with the accounts service
Admin City does not replace the accounts service, and the service does not replace Admin City.
The two common patterns are:
Pattern A: your backend owns login
- your backend validates user credentials or session
- your backend uses
Admin Cityto requestuser_token - the frontend receives
city_id + user_token
Pattern B: the accounts service owns login
- the frontend uses a guest
User Cityto callaccounts.login/register - the accounts service returns
user_tokendirectly - the frontend switches to a normal
User City
So:
Admin Cityowns trusted-side management actions- the accounts service owns the minimal auth capability
- they are not replacements for each other, but two different login patterns
env
admin.env manages runtime env values. Provider keys written here are stored in the Federation database and take priority at runtime.
If you need to inspect which provider env keys a code-registered model depends on, read the same model catalog directly through admin.listModels(), then compare it with admin.env.list():
const models = await admin.listModels();list()
const envs = await admin.env.list();upsert()
await admin.env.upsert({
key: "OPENAI_API_KEY",
value: "sk-xxx",
});remove()
await admin.env.remove("OPENAI_API_KEY");import()
await admin.env.import(`
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://api.openai.com/v1
`);refresh()
await admin.env.refresh();When env values are changed through admin.env.upsert(), remove(), import(), or the fed admin workspace, the current Runtime cache is updated automatically.
If you bypass the Admin API and edit the database env table directly, call admin.env.refresh() or run Refresh runtime cache from the fed admin workspace Env menu.
These changes are written to the env table in the Federation database. Both business env values and system-level secrets are managed from this Federation-owned table, so you no longer need to patch Worker or Node host env manually.
cities
admin.cities manages product boundaries inside the same Federation.
const cities = await admin.cities.list();
const city = await admin.cities.create({
name: "My App",
city_id: "city_my_app",
});
await admin.cities.pause(city.city_id);
await admin.cities.activate(city.city_id);
await admin.cities.remove(city.city_id);If you want to issue a token for an arbitrary city in one call, use the lower-level token manager under admin.cities:
const issued = await admin.cities.tokens.apply({
city_id: "city_my_app",
user_id: "user_123",
ttl: "7d",
});For the common case where one Admin City instance manages a single target city, prefer admin.tokens.apply(). It injects the constructor city_id automatically.
listServices() / listModels() / instruction()
Besides tokens and env, the admin side can also read the current capability catalog exposed by City.
listServices()
const services = await admin.listServices();This returns the registered service list together with each module's declared env requirements.
listModels()
const models = await admin.listModels();This returns the full model catalog from the admin view. Compared with the user-side model catalog, it also includes:
env_requirementsdefault_modes
instruction()
const text = await admin.instruction();
console.log(text);It maps to GET /v1/base/instruction and returns the aggregated plain-text City instruction document. It is useful for:
- checking which modules are currently mounted
- checking which routes each module exposes
- checking which env keys each module declares
- feeding runtime guidance into a CLI or agent
Admin City can also call service-provided admin services
Besides tokens and env, Admin City can call admin-side services exposed by official packages.
For example:
const users = await admin.service("accounts").get("users");
const sessions = await admin.service("accounts").get("sessions");
const payments = await admin.service("payment").get("payments");balance now also exposes a typed invoker for wallet and redeem_code administration:
const issued = await admin.balance.redeemCodes.create({
amount: 300,
note: "campaign gift",
});
await admin.balance.redeemCodes.disable({
redeem_code_id: issued.redeem_code_id,
});In other words, Admin City is not only for built-in token/env endpoints. It is also the trusted-side entry into the same unified service route space.
Error handling
When Admin City receives a non-2xx HTTP response, it throws an Error with two extra fields:
status: the HTTP status code.body: the raw response body from City, usually{"error":"..."}.
try {
await admin.tokens.apply({
user_id: "user_123",
});
} catch (error) {
const status = error instanceof Error && "status" in error ? error.status : undefined;
const body = error instanceof Error && "body" in error ? error.body : undefined;
console.log(status, body);
}Common statuses:
401:admin_secret_keyis missing or wrong.403: the target city is paused, so no token can be issued.404: the target city does not exist.500: City is missing required config, or the admin action failed internally.
What Admin City does not manage right now
These do not belong to Admin City yet:
- model configuration
- direct edits to the
modelstable - service handler registration
- direct frontend login interaction
- browser-side user-context calls
Those belong to:
- the database layer
- the
Cityruntime layer
In other words, Admin City owns env maintenance; runtime model definitions and mounting still belong to the City runtime layer.