Kadence International, full-time
Kadence Internal Tools
- Web
Internal Tools is Kadence's expense platform, for internal use. Each Kadence division is its own workspace, so each division's claims stay private to that division. Staff file purchase orders, reimbursements and settlements inside their division's workspace, attach items, vendors and approvers, and export each claim as a PDF. I led the project as Lead Engineer and built the API. NestJS on MongoDB with BullMQ jobs on Redis, and PDFs rendered by headless Chrome and stored on S3. A teammate built the Next.js web app.
- Where
- Kadence International, full-time employment
- Role
- Lead Engineer. I built the API from scratch. A teammate built the Next.js web app, with some contributions from me.
- Team
- Me, 1 web front-end engineer, 1 designer and 2 interns (1 engineering, 1 design). I led the project. I reviewed the team's code and coordinated with project managers, QA and operations.
- Dates
- Jul 2024 – Oct 2024
- Platform
- Web
- Stack
- NestJS 10, TypeScript 5.6, MongoDB (Mongoose 8), BullMQ on Redis, Puppeteer, AWS
- claim types
- 3
- roles
- 7
- data models
- 22
- tenancy guard on every route
- 1
01Context and my role
I led Internal Tools as Lead Engineer and built the API from scratch (every project commit since the work started on it), including the service architecture and CI/CD. A teammate built most of the Next.js web app. About a quarter of the web commits are mine.
- Jul 2024Project starts
I led the project and started the API.
- Oct 2024Work ends
The Internal Tools dates end here.
02What it does
Workspaces
Each Kadence division is its own workspace. Staff switch workspaces, see a list of recent ones, and can turn the claim system on or off per division. A workspace has members (owner or member).
Claims, in 3 types
Purchase orders, reimbursements and settlements. Each claim gets an automatic running number with a prefix by type: PO / RMB / STL. A purchase order has 8 categories. A settlement must point at an existing purchase order. On the web form, a settlement can record cash advanced.
Claim items and fields
Items (item, price, quantity), one of 4 currencies, optional PPN, a date, an optional vendor, a project number and name. The API stores lists of approvers and reviewers. On the web those roles are Approver and Confirmer.
Vendors
A shared vendor list grouped by category.
Admin
Admins manage users and workspaces. Finance can see every claim in the workspace.
Preview and PDF
Staff preview the claim document in the app, then the API renders it to an A4 PDF and stores it on S3.
Accounts
Users, 7 roles (super admin, admin, user, manager, director, finance, HR), activity log, login history, sessions, password history and expiry, password reset, verification, API keys, settings and a country list. Google sign-in.
03How it works
Filing a claim
Tenancy and access
- Tenancy isolation: every workspace request carries a workspace header. A guard checks it against the user's membership before the controller runs.
- Permissions: CASL abilities on subjects such as workspace, claim and user, combined with the 7 roles. Finance can see every claim in the workspace.
- Route groups: public, user, admin, system and shared, with two kinds of API key (default and system).
- Auth: JWT access and refresh tokens, sessions stored in the database, Google sign-in.
Jobs, documents and uploads
- Background jobs: BullMQ queues on Redis for email and session work, with priority levels.
- PDF rendering: a Handlebars HTML template rendered to A4 by Puppeteer (headless Chrome), then uploaded to S3.
- Uploads: S3 presigned and multipart uploads.
Data
- A repository layer over Mongoose, offset pagination with filters, and a Redis cache.
Web app (built by a teammate)
A teammate built most of the Next.js web app. About a quarter of the web commits are mine. I did not build these screens.
- The web app has a workspace switcher, a list of recent workspaces, and a claim-system toggle per division. Claim forms include cash advanced on a settlement that already points at a purchase order. Staff preview the claim document in the app before the A4 PDF export. Admins manage users and workspaces.
- Next.js 14 App Router, Redux with thunk and persisted state, Fluent UI, Tailwind, and Formik and Yup forms. Separate API services for the admin, system and user routes.
04Decisions and trade-offs
Access
One tenant per division
- Why
- Users wanted each division's data private to that division, so each division is a workspace. A workspace header plus a guard enforces the tenancy on every route, so a controller cannot forget it.
CASL and the 7 roles
- Why
- Finance can see every claim in the workspace. Other roles see their own. CASL abilities on workspace, claim and user sit on top of membership.
Product
Settlement points at a purchase order
- Why
- A settlement records cash advanced against a purchase order that already exists.
Infrastructure
PDF from HTML
- Why
- Designers and developers edit a Handlebars template instead of drawing PDFs in code.
- Cost
- Running headless Chrome on the server.
Queued email
- Why
- Sending mail from a BullMQ job keeps requests fast and retries failed sends.
Shared layer from ack-nestjs-boilerplate
- Why
- Auth, API keys, pagination and the rest of the shared layer were already in my starter, so this API starts from that.
- Cost
- Of 107 Jest test files, 1 is specific to claims.
Presigned upload to S3
- Why
- The browser uploads straight to S3 with a presigned URL, so the file does not pass through the API.
05Testing and delivery
- Tests: 107 Jest test files in the API. Most cover the shared layer from my
ack-nestjs-boilerplate(auth, API keys, pagination and so on). 1 is specific to claims. - CI/CD (GitHub Actions): tests on a Node.js version matrix, lint, and a release workflow that tags a GitHub release from the version number. A deploy workflow on main and staging builds a two-stage Docker image with Buildx, pushes it to Docker Hub, adds the runner's IP to the AWS security group, then connects to the EC2 host over SSH to pull and restart.
- Local quality gates: Husky, commitlint, ESLint.
06Stack
- API
Node.js 20
TypeScript 5.6
NestJS 10
Mongoose 8 (MongoDB)
Swagger
- Jobs and cache
BullMQ 5
Redis
- cache-manager
- Documents
Puppeteer 23 (headless Chrome)
Handlebars
- Cloud
AWS S3
AWS SES
SendGrid
- Auth and access
JWT
Google sign-in
- API keys
CASL
- Security and ops
- Helmet
NestJS Throttler
Sentry
- Web app (teammate)
Next.js 14
React 18
TypeScript
Redux
- Fluent UI
Tailwind CSS
Formik
- Yup
Axios
Storybook
Vitest
- Testing
Jest 29
- Delivery
Docker
Docker Hub
GitHub Actions
AWS EC2