Skip to content

andre-christi-kan, portfolio

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.

  1. Jul 2024Project starts

    I led the project and started the API.

  2. 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

The API and the systems around itThe API, built on NestJS, in the centre. The browser loads the Next.js web app, built by a teammate, which calls the API over REST with a JWT and a workspace header. The browser uploads files straight to AWS S3 with a presigned upload. The API keeps its data in MongoDB, uses Redis for cache and job queues, and reports errors to Sentry. It hands claims to a headless-Chrome PDF renderer, which saves the A4 PDF to S3. A BullMQ queue worker takes jobs from Redis and sends mail through the email providers, AWS SES and SendGrid.loadsREST, JWT +workspace headerpresigned uploaddatacache, jobsjobssend mailclaimA4 PDFerrorsBrowserWeb app, Next.js(built by a teammate)APINestJSMongoDBRediscache and queuesQueue workerBullMQPDF rendererheadless ChromeObject storageAWS S3EmailAWS SES, SendGridError monitoringSentry
A request hits the Next.js web app, then the API with a JWT and a workspace header. The API reads MongoDB, uses Redis for cache and BullMQ jobs, renders the claim PDF with headless Chrome, and stores it on S3. Solid boxes are the API and its parts. Dashed boxes belong to a teammate or to outside providers.

Filing a claim

Filing a claimStaff switch workspace in the web app. The web app calls the API with a JWT and a workspace header. A guard checks membership. Staff file a claim with items, an optional vendor, an Approver and a Confirmer. The API stores it and assigns a running number. Staff preview the document, then the API renders an A4 PDF from a Handlebars template and stores it on S3.Object storagePDF rendererAPIWeb appStaffWorkspaceClaimswitch workspaceJWT and workspaceheadermembership checkeditems, vendor,Approver, Confirmercreate the claimrunning numberpreview the documentexport as PDFHandlebars to A4store the PDFPDF stored
Staff switch workspace. The web app calls the API with a JWT and a workspace header, and a guard checks membership. They file a claim with items, an optional vendor, an Approver and a Confirmer. The API stores it with a running number. They preview the document, then the API renders an A4 PDF and stores it on S3.

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.
CI/CD for the APIA push to GitHub starts GitHub Actions. One set of workflows runs the tests on a Node.js matrix and lint, and a release workflow tags a GitHub release from the version number. The deploy workflow, on main and staging, builds a two-stage Docker image with Buildx and pushes it to Docker Hub, allows the runner's IP in the AWS security group, then connects to the AWS EC2 host over SSH. The host pulls the image from Docker Hub and restarts.API onAWS EC2AWS securitygroupDocker HubGitHubActionsGitHubTests and releasetests on aNode.js matrix,lintDeploy, on main and stagingDocker Buildx,two-stage buildrestartpushGitHub releasefrom the versionpush imageallow runner IPSSH inpull the image
CI/CD for the API, from a push to the running container. The infrastructure is kept apart from the architecture in How it works.

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