Ascenda Wiki
Ascenda · reference

API surface

One NestJS service, five controllers, and a guard chain that runs before all of them. Authentication is the default; @Public() is the exception you have to write down.

23 routes 5 controllers 4 public 60/min default throttle
the chain

What runs before your handler

Four global guards, in registration order — the order is the argument. Throttle first, so an unauthenticated flood is dropped before it costs a JWKS lookup. Then authenticate the token. Then resolve the token subject into a user row with its roles. Only then authorize.

  • 1ThrottlerGuard60 req / 60s, tighter per route
  • 2ClerkJwtGuardverifies against Clerk JWKS
  • 3UserResolutionGuardsubject → user id + roles
  • 4RolesGuardreads @Roles, DB-backed
@Public() skips authentication, not throttling. @AuthSubject() injects the Clerk subject, @CurrentUserId() the resolved row id — a handler that takes the second one cannot run before guard three.
transactions

/transactions

Every route is scoped to the caller. Categorization is per-user - the same merchant can mean different things to two people - so even the read-only resolve endpoints need an identity.

src/modules/transactions/transactions.controller.ts 9 routes
  • GET /transactions — List, filtered by the query DTO bearer
  • POST /transactions — Create one; resolves the merchant inline bearer
  • POST /transactions/batch — Create many - a statement import bearer
  • POST /transactions/categorize — Resolve one descriptor, write nothing bearer
  • POST /transactions/categorize/batch — Resolve many; the import screen calls this first bearer
  • GET /transactions/:id — Declared after the static routes, or categorize is captured as an id bearer
  • PATCH /transactions/:id — Edit; a category change is the signal that teaches the dictionary bearer
  • DELETE /transactions/:id — Soft delete bearer
  • POST /transactions/:id/repeat — Clone onto a new occurredAt bearer
onboardings

/onboardings

The only public surface of consequence. An onboarding exists before an account does, so the session is held by a durable secret in x-onboarding-secret rather than a JWT - and the write routes are rate-limited harder than the global default.

src/modules/onboardings/onboardings.controller.ts 5 routes
  • GET /onboardings/goal-types — Seeded lookup public
  • GET /onboardings/intent-types — Seeded lookup public
  • POST /onboardings — Start a session; returns the onboarding secret public · 5/min
  • PATCH /onboardings/:id/progress — Advance one step session secret
  • POST /onboardings/:id/convert — Attach the finished session to the account that just signed up bearer + secret · 10/min
budget

/budget

A cycle is one run of the budget algorithm, stored with the inputs it ran on. The simulator is public because it computes from the body alone and touches nothing.

src/budget/budget.controller.ts 5 routes
  • POST /budget/cycles — Run and persist a cycle for a month bearer
  • POST /budget/cycles/simulate — Run against a supplied body, persist nothing public
  • GET /budget/cycles/current — Literal route before :id, or "current" matches as an id bearer
  • GET /budget/cycles/:id — One cycle in full bearer
  • GET /budget/cycles — Summaries, newest first bearer
users

/users

Roles are read from the database, not from the token, so a role change takes effect on the next request rather than the next login.

src/modules/users/users.controller.ts 3 routes
  • GET /users — Staff list INTERNAL_ADMIN | INTERNAL_STAFF
  • GET /users/me — The caller bearer
  • PATCH /users/me — Update own profile bearer
webhooks

/webhooks

Public to the network, closed by signature: the raw body is verified against CLERK_WEBHOOK_SIGNING_SECRET with the svix headers before anything is read from it. Nest is bootstrapped with rawBody: true for exactly this.

src/modules/webhooks/clerk-webhook.controller.ts 1 routes
  • POST /webhooks/clerk — user.created / updated / deleted svix signature
transport

Bootstrap facts worth knowing

Docs
/api
Port
PORT → 3000
Validation
AppValidationPipe, global
Errors
HttpExceptionFilter, global
CORS origins
localhost:8081, :19006
Extra headers
x-onboarding-secret, x-idempotency-key
The CORS allowlist is literal localhost origins. A device on the LAN reaches the API by IP, which is why the mobile app carries its own base URL in .env rather than assuming localhost.

Checking this page. Every row above corresponds to a decorator in the file named in its panel bar. The live contract is the Swagger document at /api; this page exists to be readable, not to replace it.