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 fileResponsibility
routes/users.tsAdmin user management wrapping better-auth admin APIs (create, update, ban, and read-only list/get/count), plus addFcmToken ($addToSet onto the user's fcmTokens, for push notifications) and getUserTenants (the tenants a given user belongs to).
routes/me.tsget returns the current authenticated user, plus phone-number verification - see below.
routes/home.tsHealth / status endpoint.

Phone-number verification (routes/me.ts)

EndpointBehaviour
setPhoneNumberStores an E.164 number with a 6-digit code and texts it via SMS. Rate-limited to one request per minute by a conditional updateOne - which also means re-submitting the number already on file is rejected.
verifyPhoneNumberChecks the code and flips phoneNumberVerified.

Both require SMS.onSendSms to be configured. thunder-core ships no SMS provider - see SMS.

Shipped Hooks

HookPriorityResponsibility
hooks/essentials.tsHighestCORS (preflight + headers, allowing X-TENANT-ID, X-API-KEY and X-PAYLOAD-SIGNATURE) and the DB-backed env fallback (Env.onGetFailed). Its corsHeaders record is exported, so your own hooks and routes can reuse it.
hooks/guard.tsVery highRBAC enforcement on every route via withAuthGuard.
hooks/requestLogs.tsLowestLogs 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.

Fields worth knowing

  • schemas/tenantMember.ts carries an optional expiresAt (temporary memberships) and exports the isMembershipExpired predicate; schemas/tenantInvite.ts carries the matching optional membershipExpiresAt.
  • schemas/user.ts holds the non-better-auth extras: fcmTokens (push tokens), phoneNumber (E.164), phoneNumberVerified, phoneNumberVerificationCode and phoneNumberVerificationRequestAt.

Some fields carry Zod .meta({ canFilter: true, label: "..." }). That is consumed by Thunder UI to offer filters on a collection - the API itself ignores it. Currently applied in schemas/user.ts (name, email, emailVerified, phoneNumber, role, createdAt) and schemas/walletLedger.ts (type, reference, amount, createdAt, updatedAt).

Common Consumer Pattern

Inside your own routes, gate access and read context like this:

routes/billing.ts
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 a policy and attach that policy to the appropriate role(s). The preferred way is ACManager from your own setup code - see Granting a New Permission. Remember the 30-second permission cache.


On this page