Routes & Schemas
Shipped routes, hooks, collections, and the common consumer pattern for using thunder-core in your own code.
A quick map of everything thunder-core ships. As always, read the source files for exact request/response shapes and model fields.
Users, Me & Status
| Route file | Responsibility |
|---|---|
routes/users.ts | Admin user management wrapping better-auth admin APIs (create, update, ban, and read-only list/get/count). |
routes/me.ts | Returns the current authenticated user. |
routes/home.ts | Health / status endpoint. |
Shipped Hooks
| Hook | Priority | Responsibility |
|---|---|---|
hooks/essentials.ts | Highest | CORS (preflight + headers, allows X-TENANT-ID) and the DB-backed env fallback (Env.onGetFailed). |
hooks/guard.ts | Very high | RBAC enforcement on every route via withAuthGuard. |
hooks/requestLogs.ts | Lowest | Logs every response at the appropriate level. |
Schemas / Collections
All models use the framework's mongodb instance with a Zod schema. Read each schema file for exact fields, defaults, and unique indexes (scripts/syncDBIndexes.ts):
schemas/user.ts, schemas/session.ts, schemas/tenant.ts, schemas/tenantMember.ts, schemas/tenantInvite.ts, schemas/accessControl.ts, schemas/accessControlPolicy.ts, schemas/apiKey.ts, schemas/oauthClient.ts, schemas/oauthConsent.ts, schemas/jwk.ts, schemas/env.ts, schemas/references.ts, schemas/wallet.ts, schemas/walletLedger.ts.
resourceGrantSchema (exported from schemas/oauthConsent.ts) maps a tenant id to granted scopes, and is reused by both OAuth consents and API keys to scope access per tenant.
Common Consumer Pattern
Inside your own routes, gate access and read context like this:
import { Router } from "@/core/http/router.ts";
import { withAuthSession } from "@/plugins/Huruf-Tech/thunder-core/utils/withAuthSession.ts";
import { withTenant } from "@/plugins/Huruf-Tech/thunder-core/utils/withTenant.ts";
import { Wallet } from "@/plugins/Huruf-Tech/thunder-core/lib/wallet.ts";
export default new Router("/api", function billing(router) {
router.post("/charge", function charge() {
return async (req) => {
// Auth + tenant resolution (guard hook already enforced "billing.charge" permission)
const { user } = await withAuthSession(req);
const { tenant } = await withTenant(req);
await Wallet.transfer({
fromTenant: tenant._id,
toTenant: platformTenantId,
currency: "usd",
amount: 500,
user: user._id,
purpose: "subscription",
});
return Response.ok();
};
});
}).group("Billing");To make a new route's permission available, add its scope.action (router fn name + handler fn name) to an access-control policy and attach that policy to the appropriate role(s) - via the access-control routes or by editing scripts/syncAccessControl.ts.