Greezmate
A multi-tenant SaaS platform for automotive workshops — whether they are starting fresh or moving off legacy software — built on a serverless AWS backend — with tenant isolation enforced at every layer, a repeatable path for migrating each workshop’s history, and a recovery plan defined in code.
- Role
- Co-developer
- Type
- Product
- Timeline
- 2025 — present
- Stack
- NestJS · AWS Lambda · API Gateway · PostgreSQL · Cognito · AWS CDK
The problem
Greezmate serves two kinds of workshop: new customers starting fresh, and established workshops moving off legacy workshop management software. For the second group, adopting a new platform means more than learning new software — years of customers, vehicles, jobs, quotes, invoices and payments have to come with it, and the legacy data is stored in a proprietary binary file format.
Four requirements shaped the design:
- Multi-tenancy. Many workshops share one platform, and no workshop can ever see another’s data.
- Low running cost. As an early-stage product, infrastructure cost had to stay proportional to actual usage.
- Legacy data onboarding. For workshops moving from legacy software, their history had to be converted from binary files and imported accurately, in a way that could be repeated for every tenant.
- Recoverability. Workshop records include invoices and payments, so data loss had to be bounded, and older records had to be retained for the long term.
The approach
A serverless AWS backend, with every resource defined in AWS CDK and separate staging and production environments.
Multi-tenant API
The API is a NestJS application packaged as a container image and run on AWS Lambda behind API Gateway. Each business domain — customers, vehicles, jobs, quotes, invoices, technicians — is its own NestJS module with a consistent controller, service, entity and DTO structure.
Every route is prefixed with the tenant ID, and every database table includes tenant_id in its composite primary key. The data store is PostgreSQL on Amazon RDS, with schema changes versioned through Flyway and applied from the CI pipeline.
Identity and access
Users sign in through Amazon Cognito. A pre-token-generation Lambda adds the user’s role and tenant to their tokens, so every request carries who the user is and which workshop they belong to.
An authorization guard in the API checks those claims on every request. Each endpoint declares the action it performs, and the guard compares it against a tiered role model — workshop manager, tenant administrator and system administrator. Independently of that, any route scoped to a tenant is rejected if the tenant does not match the user’s own.
Asynchronous work
Work that does not belong in a request-response cycle runs separately. Document generation is queued on SQS and handled by a worker Lambda that renders PDFs to S3. Scheduled jobs, such as the nightly quote-expiry sweep, are triggered on a schedule through a dedicated Lambda that calls the API. Customer notifications go out by email and SMS, with SMS delivery receipts recorded for tracking.
Legacy data migration
I built the pipeline that brings each workshop’s history across. It runs in two stages, each shipped as its own Docker image and run as an ECS Fargate task:
- Conversion. A converter reads the legacy binary files from a per-tenant S3 prefix and writes them back as CSV.
- Import. The migration stages the CSV files in DuckDB, transforms them, and loads them into PostgreSQL in dependency order — makes and models, customers and vehicles, technicians, jobs, invoices, payments and quotes.
Every step is idempotent, so a migration can be re-run safely, and the final step validates the migrated customer balances. Database credentials are read from Secrets Manager at run time, so nothing sensitive is built into the images.
Backup and recovery
I also designed the backup and recovery approach, with defined recovery objectives for each failure scenario. PostgreSQL is protected in layers: RDS point-in-time recovery, a manual snapshot before every Flyway migration, daily and weekly AWS Backup recovery points, and a monthly logical dump written to an S3 bucket under Object Lock and transitioned to Glacier Deep Archive for long-term retention.
In production, the backups are protected from deletion as well — the Backup vault denies the deletion of recovery points, and stateful resources are retained if their stack is removed. A restore script and runbook cover the full procedure: freeze writes, restore to a new instance, verify row counts, cut over by renaming, and resume traffic.
What I learned
- Legacy data migration takes longer than it looks. It is easy to treat migration as a one-off task, but understanding, mapping and validating years of another system’s data takes real time.
- Build a pipeline, not a script. With access only to the legacy data — never the legacy source code — the meaning of every field has to be worked out from the data itself. On top of that, some workshops had customised their legacy system, which meant customer-specific adjustments to the migration. A modular pipeline with extendable logic absorbed those differences far better than a collection of one-off scripts would have.
Key decisions
Tenant ID in every primary key
All tenants share one PostgreSQL database, so every table carries tenant_id as part of a composite primary key and every API route is scoped by tenant. Isolation is enforced by the data model itself, not only by application code.
Trade-off: Every key, query and migration has to carry the tenant, and backups restore the whole database — recovering a single tenant is not yet possible.Asynchronous document rendering
Generating a PDF takes time, so it runs outside the request-response cycle. The API places a job on an SQS queue, a worker Lambda renders the document to S3, and the client receives a presigned URL once it is ready.
Trade-off: Clients have to poll for completion, and local development needs LocalStack to reproduce the queue-and-bucket flow.A containerised legacy-data pipeline
Workshop history arrives in a proprietary binary file format. Conversion and import run as two separate container images on ECS Fargate, triggered per tenant, so every onboarding follows the same repeatable process.
Trade-off: Two images and a CDK stack to maintain for a job that runs once per tenant.Restore to a new instance, then swap
Every database restore builds a new instance from point-in-time recovery or a snapshot, verifies it against the live database, and swaps it in by renaming. The damaged instance is kept, and connection details do not change.
Trade-off: The API is frozen from start to cutover, so a restore means a planned outage of up to two hours.