Skip to content

andre-christi-kan, portfolio

Open source

ack-nestjs-boilerplate

  • Ongoing
  • MIT
  • v9.1.0

A NestJS starter for teams that would rather not rebuild authentication, permissions and multi-tenancy for every new service. Every route passes the same ordered chain of middleware and guards, so a new feature gets throttling, auth, policy checks and tenancy without extra code.

Where
Open source, my own project
Role
Sole maintainer. I review contributor pull requests.
Dates
Jan 2022 – Present
Stars
700+ on GitHub
Licence
MIT
Release
v9.1.0
Stack
NestJS 12, TypeScript 6, Prisma 6, MongoDB, Redis, BullMQ, Zod 4
GitHub stars
700+
unit coverage
100%
GitHub Actions workflows
9
guides in docs/
32
The module map of ack-nestjs-boilerplateTwo entry points at the top: the route groups (public, user, admin, system and shared) send HTTP requests, and the processor router runs the BullMQ workers and sends jobs. Both lead into the 19 feature modules: auth, session, user, device, password history, role, policy, API key, term policy, feature flag, workspace, project, onboarding, notification, activity log, analytics, country, health and hello. The feature modules sit on a common layer for the database, Redis, cache, AWS, Firebase, pagination, request and response handling, logging, messages, Sentry and docs. The common layer talks to MongoDB (a replica set), Redis, AWS S3 and SES, and Firebase Cloud Messaging. An optional HashiCorp Vault, drawn above it, supplies secrets to the common layer.HTTPjobssecretsRoute groupspublic, user, admin,system, sharedProcessor routerruns the BullMQ workers19 feature modulesauth, session, user,device, password history,role, policy, API key,term policy, feature flag,workspace, project,onboarding, notification,activity log, analytics,country, health, helloCommon layerdatabase (Prisma), Redis,cache, AWS (S3, SES),Firebase, pagination,request, response,logger (Pino),messages (i18n), Sentry,docs (OpenAPI)MongoDBreplica setRedisAWS S3, SESFirebase CloudMessagingHashiCorp Vault(optional)
The module map. Solid boxes are part of the boilerplate. Dashed boxes are outside services.

01Context and my role

I started ack-nestjs-boilerplate in January 2022 and have maintained it on my own since then. It is my own project, not employment or client work. It is released under the MIT licence, has 700+ stars on GitHub, and the current release is v9.1.0. My own work projects started from it: Kadence K-⁠OINS, Kadence Internal Tools, Korner and Field Operation Platform.

02Focus

ack-nestjs-boilerplate is a modular NestJS starter. It exists so a team does not rebuild authentication, permissions and multi-tenancy for every new service. A few choices shape it: stateful authentication and authorization, a repository layer between the domain code and Prisma, configuration through environment variables in the Twelve-Factor style, native ESM, and tenancy by workspace, with projects scoped to a workspace.

  • Enterprise apps: roles, CASL policies and an activity log.
  • Auth services: JWT, Google and Apple sign-in, and TOTP two-factor.
  • Mobile backends: a REST API with social sign-in, device tracking and push notifications.
  • Multi-tenant SaaS: every user belongs to a workspace, and projects stay scoped to it, with invites and join requests.
  • Startup MVPs: auth, workspaces, notifications and file upload already wired.

03Features

  • Authentication

    JWT signed with ES256 (access) and ES512 (refresh), Google and Apple sign-in, one-time passwords, TOTP two-factor with backup codes.

  • Sessions

    Redis-backed sessions that can be revoked. A stolen token dies on the next request once the session is gone.

  • Permissions

    A role is not enough. Each person also has abilities: who may invite, who may read a project, who may change a flag. Those abilities travel with the product, not with one screen.

  • Feature flags

    Turn a feature on for a share of people without a new release. Rollout is a percentage, not a deploy.

  • API keys

    Machine access with the same protection as a signed-in person, so a job or a partner can call in without a password.

  • Multi-tenancy

    Every user belongs to a workspace. Projects stay scoped to it, with invites, membership and join requests.

  • Database

    Prisma on MongoDB (a replica set, so transactions work), a repository layer, Redis cache, offset and cursor pagination.

  • Storage

    Files reach S3 two ways: presigned URLs for GET, upload and multipart parts, so the bytes skip the API, or multipart upload through the API, which then writes to S3.

  • Background jobs

    BullMQ takes email, push and logs off the request. The response returns; workers retry the slow work.

  • Notifications

    In-app and silent notifications, email through AWS SES, push through Firebase Cloud Messaging.

  • Observability

    Pino logs each request. Sentry records failures. Bull Board shows the BullMQ queues. Activity logs, admin and per-workspace analytics, and health checks show what ran.

  • Operations

    Rate limiting with a sliding window in Redis, Helmet, Zod request contracts, i18n, OpenAPI 3.1 docs, optional HashiCorp Vault for secrets.

  • CI/CD

    GitHub Actions for lint, tests and releases to AWS EC2, AWS ECS or any server over SSH, with Docker.

  • Tooling

    Native ESM on Node.js 24 and TypeScript 6, SWC builds, Vitest.

  • AI

    I wired Claude Code into the repo: AGENTS.md, skills (ack-build, ack-doc, ack-plan, ack-harness, ack-pr, ack-review, ack-spec) and agents (coder, debugger, explorer, harness, reviewer, tester, writer), so an agent can change the starter the same way I do.

04Why start from it

  • My own backends start from it: Kadence K-⁠OINS, Kadence Internal Tools, Korner and Field Operation Platform started from this code. Their auth and API keys came from its shared layer.
  • Integrations stay off until set: AWS S3 and SES, Firebase, Sentry, and Google and Apple sign-in are optional. A blank line in .env leaves one unset and the API still boots. A route that needs an unset one answers 404.
  • Docs from the route schemas: Swagger UI and generated/swagger.json are built from the same route schemas, in OpenAPI 3.1. Swagger is off in production.
  • One guard order: a route declares only the protection decorators it needs, and they always run in the same relative order.
  • 32 guides: installation, configuration, every core module and release each have their own guide in docs/.
  • Clone, tweak, run: you do not write a report or a spec to get a backend. Auth, tenancy, jobs, storage and docs are already there. Clone the repo, set the env you need, and use it.
  • AI workflow included: Claude Code is wired in: AGENTS.md, skills and agents, so an agent can change the starter the same way I do.

05How it works

A request lands on one of the route groups: public, user, admin, system or shared. Workers sit on a separate processor router. Before any handler, the pipeline is the same.

  • Middleware, in order: request ID, request log, Helmet, body parser, CORS, URL versioning, response time, language, workspace context, then compression.
  • Throttle: default then per-route, sliding window, counters in Redis.
  • Route guards, in order: API key, JWT checked against the Redis session, feature flag, user, workspace and membership, project and membership, role and CASL policy, terms accepted. Nest runs the protection decorators bottom-up, so that is the order they execute.
  • Validation: a Zod pipe on the body.
  • Interceptors: timeout and actor.
  • Handler: controller, then domain service, then repository, then Prisma, then the MongoDB replica set, with Redis cache beside it. The response returns from there.
  • Side effects: email, FCM push, in-app and silent notifications and activity logs leave as BullMQ jobs. Processors send them after.
  • Errors: global exception filters. Sentry gets the failure, and the client always gets the same error shape.
One request, from the first middleware to the databaseA client request enters the pipeline. Step 1 is the middleware chain in order (request ID, logging, Helmet, CORS, versioning, i18n and workspace context). Step 2 is the throttle guards, default then per route, with limits in Redis. Step 3 is the route guards in order: API key, JWT checked against the session in Redis, feature flag, user, workspace and its membership, project and its membership, role and policy, then terms. Step 4 is Zod validation and step 5 the timeout and actor interceptors. Step 6 reaches the controller and domain service, which reads the Redis cache and goes through the repository and Prisma to the MongoDB replica set, then returns the response. Step 7 adds side-effect jobs to BullMQ queues, whose processors send email through SES, push through FCM and in-app notifications. Step 8: any error goes to the exception filters, which report it to Sentry and give the client the same error shape on every route.BullMQprocessorsRepository,Prisma,MongoDBControllerand serviceRedisRequestpipelineClientBefore the handlerAPI key, JWT,feature flag, user,workspace, project,role and policy,termsIn the handleremail (SES),push (FCM),in-appexception filtersreport to Sentryalt[any error]request1. middlewarechain2. throttle limits3. route guards,JWT session4. Zod validation5. interceptors:timeout, actor6. call the handlercacheread, write7. side-effect jobsresponse8. same error shape
One request, from the first middleware to the database. Every route passes the same guards in the same order. Slow work leaves the request as a queued job.

06Decisions and trade-offs

Business

Feature flags with percentage rollout

Why
A feature can reach a share of people without a new release.
Cost
Rollout identity depends on guard order. Run the flag before sign-in and a percentage targets an anonymous header, not an account.

Infrastructure

Asymmetric JWT keys (ES256 access, ES512 refresh)

Why
Another service can verify a token with the public key and never holds the signing key.
Cost
If JWKS is unreachable, signed-in users get 503, not a token error. Local compose has to run that endpoint too.

Sessions in Redis behind signed tokens

Why
Every request checks the signature and the Redis session, so revoking a session takes effect on the next request.
Cost
Redis sits on the auth hot path. Redis down, and a valid token still fails.

MongoDB as a replica set, even locally

Why
Prisma needs a replica set for transactions, so local development matches production.
Cost
A laptop runs a replica set. A single-node Mongo from a tutorial will fail transactions.

Prisma for the repository pattern

Why
Domain services talk to a repository, not to the database, so a later database change is a repository swap, not a rewrite of every service.
Cost
A later database swap is still a Prisma schema rewrite. Only the domain services stay untouched. Until then every query is Prisma's generated client.

Presigned S3 uploads

Why
The client sends and fetches files straight to and from S3, so the bytes never pass through the API.
Cost
Untrusted bytes land in the bucket. The API cannot reject a file by looking at it. Access and expiry live on S3.

Async work on BullMQ

Why
Email, push and activity logs must not hold the response. A worker retries if the process dies.
Cost
HTTP 200 can land before the email exists. If workers are down, the user sees success and the queue waits.

Other

Guards as a fixed chain

Why
Every route passes the same ordered checks, so a new feature inherits throttling, auth, policy, terms and tenancy without extra code.
Cost
You cannot reorder one check for one route. Put the flag below JWT and rollout follows an anonymous id, not the signed-in user.

Observability

Why
A request, a crash and a queued job need different views. Pino logs the request. Sentry records the crash. Bull Board shows the queue.
Cost
Three UIs to operate. Skip Sentry and crashes stay in the log. Skip Bull Board and a stuck job is invisible.

Firebase Cloud Messaging for push

Why
One push path covers iOS, Android and the web. The send runs on a BullMQ job so the request does not wait on FCM.
Cost
Push is Google's availability. No FCM key, and those routes 404. iOS, Android and web all go dark together.

AI workflow in the repo

Why
I wired Claude Code into the starter: AGENTS.md, skills (ack-build, ack-doc, ack-plan, ack-harness, ack-pr, ack-review, ack-spec) and agents (coder, debugger, explorer, harness, reviewer, tester, writer), so an agent follows the same workflow I use to change the project.
Cost
The harness is Claude Code's. Cursor or Codex does not run those skills until someone maps the files.

07Testing and delivery

  • Tests: a Vitest unit suite in test/unit/, 705 spec files. A 100% coverage threshold on branches, functions, lines and statements. Controllers, repositories, modules, processors and generated code sit outside it.
  • GitHub Actions, 9 workflows: linter runs on every pull request. test-unit, test-integration, test-e2e, release-version, release-with-aws-sdk-ec2, release-with-aws-sdk-ecs and release-with-ssh start by hand. stale runs daily.
  • Docker image and local compose with a MongoDB replica set and optional Vault.
Workflows and release targetsA pull request runs the GitHub Actions linter workflow. The other workflows are started by hand: the Vitest unit tests, and a versioned release that builds the Docker image, which the release workflows deploy to AWS EC2, AWS ECS or any server over SSH. For development, a local compose file runs a MongoDB replica set and an optional HashiCorp Vault.releasereleasereleasePull requestLinterStarted by handUnit tests(Vitest)Versioned releaseDocker imageAWS EC2AWS ECSAny serverover SSHFor developmentlocal compose:MongoDB replica set,optional HashiCorp Vault
The GitHub Actions workflows and where a release can go. Kept apart from the module map above.

08Stack

Runtime and language
  • Node.js 24
  • TypeScript 6
  • native ESM
  • SWC
  • pnpm
Framework
  • NestJS 12
  • OpenAPI 3.1
Data
  • Prisma 6
  • MongoDB (replica set)
  • Redis
Validation
  • Zod 4
Jobs
  • BullMQ
Auth and access
  • JWT (ES256, ES512)
  • Google sign-in
  • Apple sign-in
  • OTP
  • TOTP
  • CASL 7
  • API keys
Cloud
  • AWS S3
  • AWS SES
  • Firebase Cloud Messaging
  • HashiCorp Vault (optional)
Ops
  • Helmet
  • sliding-window rate limiting (Redis)
  • Sentry
  • Pino
  • health checks
  • i18n
Testing
  • Vitest 5
Delivery
  • Docker
  • GitHub Actions (EC2, ECS, SSH)

09Documentation

This page is the engineering story: what the boilerplate is for, how a request moves through it, and why it is built this way. It is not the manual.

How to install, configure and use it lives in the repository, in 32 guides under docs/. Start at docs/readme.md, which lists them in order from installation to release.