The API with accounts
You are holding the password now. That is one hash, one reset flow, one rate limit on the login route, and one decision you cannot take back quietly.
signup and login are route handlers, not a framework — they take your table and your field names and hand you the user. What you do with the user is yours: the token below is a JWT, but a database-backed revocable session from @atlas/security is the better default the moment you need to kick someone out.
token.sign and token.verify are async. Forgetting the await yields a Promise that stringifies to [object Promise] and a token that verifies as garbage.
What you are carrying
@atlas/serverRoutes and the pipe pipeline.
@atlas/dbThe users table.
@atlas/authHashing, JWTs, signup and login flows.
@atlas/securityRate limits, audit log, revocable sessions, TOTP.
@atlas/emailThe reset mail you will need by week two.
Start it
atlas init -n myapi --template api
What it looks like
import { token, signup, login, requireAuth } from "@atlas/auth"
import { post, pipeline, parseJson, json } from "@atlas/server"
export const routes = [
post("/auth/signup", pipeline(parseJson)(signup({
db, table: "users", fields: ["email", "name", "password"],
onSuccess: (c, user) => json(c, 201, { id: user.id, email: user.email }),
}))),
post("/auth/login", pipeline(parseJson)(login({
db, table: "users", identity: "email", password: "password",
// token.sign is async — the missing await is the bug you will not see.
onSuccess: async (c, user) =>
json(c, 200, { token: await token.sign({ id: user.id }, secret) }),
}))),
]
export const guard = requireAuth({ secret })