Accounts Service
Use @downcity/services for registration, login, OAuth entry points, profile data, and user_token issuance.
The accounts service fits studios that do not yet have a complex user system. It provides better-auth auth tables, a studio-facing profile table, password registration/login, and City user_token issuing after registration or login.
This page focuses on how the service fits into the real studio call chain, not only what tables it creates.
Install And Enable
import { accountsService } from "@downcity/services";
base.use(accountsService({
token_ttl: "7d",
}));Where it sits in the studio flow
studio frontend
-> guest UserClient with base_url only
-> call guest accounts actions
-> accounts service returns user_token
-> frontend creates a normal UserClient with base_url + studio_id + user_token
-> frontend continues with AI services or custom servicesIf you already have your own backend, you can skip the service for login itself and let your backend validate users first, then use AdminClient to issue user_token. The accounts service is the official ready-made service path for a minimal auth system.
Choosing the pattern
Pattern A: use the accounts service directly
Good fit when:
- you want the fastest path to registration and login
- you do not want to design your own auth, profile, and session tables yet
- you want registration or login to return
user_tokenimmediately
Pattern B: your backend owns login
Good fit when:
- you already have a mature account system
- you need complex org, invitation, permission, or risk-control logic
- you only want City to handle
user_tokenissuance
Both patterns converge to the same end state: the frontend gets studio_id + user_token, then uses a normal UserClient to call City.
Login
import { UserClient } from "@downcity/gate";
const guest = new UserClient({
base_url: "https://base.example.com",
});
const accounts = guest.service("accounts");
const session = await accounts.action("login").invoke({
email: "user@example.com",
password: "password123",
studio_id: "studio_demo",
});The response includes user_token, user_id, and email. The studio client can then create a normal UserClient and call AI services.
Registration
const session = await accounts.action("register").invoke({
email: "user@example.com",
password: "password123",
studio_id: "studio_demo",
});If your service configuration requires email verification, the next step is usually verify-email.
OAuth entry points
const start = await accounts.action("oauth/start").invoke({
provider: "github",
studio_id: "studio_demo",
});
const result = await guest.service("accounts").get("oauth/result", {
state: start.state,
});The studio client does not need to pass redirect_uri. The accounts service generates the third-party OAuth callback automatically from the current public City origin:
https://your-base-domain/v1/accounts/oauth/callbackSo the callback URL registered in the GitHub or Google OAuth app, or the WeChat Website App console, must match that exact URL.
If you enable WeChat login here, it specifically means the "WeChat Open Platform -> Website App -> WeChat Login" flow, not Official Account login and not Mini Program login.
If you want provider-specific setup next, continue with:
This set of endpoints usually comes in two phases:
oauth/start: get the redirect URL or stateoauth/result: poll or confirm the login result
Switching from guest UserClient to normal UserClient
const guest = new UserClient({
base_url: "https://base.example.com",
});
const session = await guest.service("accounts").action("login").invoke<{
user_token: string;
}>({
email: "user@example.com",
password: "password123",
studio_id: "studio_demo",
});
const user = new UserClient({
base_url: "https://base.example.com",
studio_id: "studio_demo",
user_token: session.user_token,
});
const reply = await user.ai.text({
prompt: "Write a welcome message",
});This switch is the most important transition in an accounts-service integration.
Route Convention
The accounts service uses the same unified /v1/* route space:
POST /v1/accounts/register
POST /v1/accounts/verify-email
POST /v1/accounts/login
POST /v1/accounts/oauth/start
GET /v1/accounts/oauth/result
GET /v1/accounts/oauth/callback
GET /v1/accounts/me
POST /v1/accounts/logout
GET /v1/accounts/users
GET /v1/accounts/sessionsregister, verify-email, login, oauth/start, and oauth/result are guest-access entry points. me and logout require a user_token.
Which Actions are guest-access
As a first mental model:
- guest-access:
register,verify-email,login,oauth/start,oauth/result - logged-in user:
me,logout - admin-side: user and session maintenance endpoints
That means:
- guest Actions still run through
UserClient - but at that stage the
UserClientonly needsbase_url - once you enter user context, you move to
studio_id + user_token
Current User And Logout
const me = await user.service("accounts").get("me");
await user.service("accounts").action("logout").invoke();logout currently returns { success: true }. If you need stricter session invalidation, extend it in your own backend or service hook.
me now returns:
user: the current runtime userprofile: studio-facing profile data
Manage Users
const users = await admin.service("accounts").get("users");
const sessions = await admin.service("accounts").get("sessions");The service now maintains:
- better-auth auth tables:
auth_users,auth_accounts,auth_sessions,auth_verifications - business-facing profile table:
auth_profiles - oauth polling table:
service_accounts_oauth_states
City users do not write these tables by hand.
Relationship with @downcity/gate
The accounts service does not replace @downcity/gate.
More precisely:
@downcity/servicesregisters theaccountsservice into City@downcity/gateis what studio-side and trusted-side callers use to call that service
So the service provides the auth capability, while the SDK still provides the calling surface through UserClient and AdminClient.