Saaim Abdullah
ProjectsJuly 15, 2026

Fitter Health: a production platform, built from zero

Built with
  • Django
  • Django REST Framework
  • PostgreSQL
  • Celery
  • Redis
  • Next.js
  • AWS
  • Amazon ECS
  • Amazon RDS
  • Amazon S3
  • Amazon SNS
  • Amazon SQS
  • IAM
  • VPC
  • Odoo
A Spain-based preventive health team had a validated concept, paying intent from both sides of their market, and no engineering function. They needed the whole platform: patient and provider accounts, two-sided session booking, a membership and credits model, invoicing reconciled with the Odoo system their operations already ran on, an admin panel, a custom CRM for leads, and the infrastructure underneath, built and run by one person. The architecture was specified by the client. My job was to implement all of it as the only engineer, and to catch what the specification had not accounted for. The code and the model are under NDA. What follows is architecture and reasoning, with no implementation detail or client data. Sole engineer, a non-technical client, and a timezone gap, so every requirement had to survive being written down: decisions were made on calls, confirmed in writing, and built in slices the founders could review as working software rather than documents. Clinical compliance rules and a large role-based access matrix constrained the data model from day one. Invoicing had to reconcile with an Odoo instance that already existed and was not going to change. And the handover was a requirement, not an afterthought: the client had to be able to run, rebuild, and extend the system without me. The domain carried its own constraints, and they were the hard part rather than the stack:
  • Memberships are entitlements, not subscriptions. A member holds a balance of credits that different session types consume at different rates, with expiry and carry-over rules. Booking has to check entitlement, not just availability.
  • Two-sided booking. Coaches and members each have calendars, and a session is only real when both sides hold. Cancellation has to return the credit under some conditions and not others.
  • Invoice state lives in Odoo. Payment status originates in more than one payment rail and has to reconcile against the platform's view of what a member is owed or owes.
Django and Django REST Framework behind a Next.js frontend, PostgreSQL as the primary store, Celery and Redis for anything that must not block a request. On AWS: containerised application instances behind a load balancer across two availability zones, RDS for PostgreSQL, S3 for files, SES for transactional mail, and SNS with SQS on the eventing path, all inside a VPC with scoped IAM roles and no public database path.
  • A Next.js frontend covering both sides of the product: the customer-facing panel and a full admin panel for operations and content.
  • A custom CRM module for leads and pipeline, built into the platform rather than bolted on from a third-party tool.
The admin panel is not a convenience. A non-technical team that has to file a ticket with their only engineer to correct a booking or adjust a credit balance does not have a working product, so operations, content, scheduling, entitlement adjustments and the compliance-sensitive record views all have first-class screens. The CRM sits inside the platform for the same reason: a lead becoming a member is then one system's state change instead of two systems disagreeing. What it does in production. It is live at fitter.health, built and operated by one engineer. Entitlement, booking and invoice reconciliation are modelled with database-level constraints, so a credit cannot be double-spent by a race between two requests. Notification delivery is durable and inspectable after the change described below, with failures landing in a dead-letter queue rather than vanishing. The infrastructure is defined and reproducible, and the client owns the AWS account, the repositories and the domains. The platform had a re-engagement requirement: members inactive for 21 days or more should get an email and a push notification nudging them back. The pipeline was event driven. A scheduled EventBridge rule ran daily, a Lambda picked out the inactive members, and the results were published to an SNS topic that fanned out to the notification consumers for email and push. The original design had SNS delivering directly to those consumers. That works right up until a consumer fails mid-delivery. From the publisher's side SNS is fire and forget: if a subscriber errors or times out, the message is retried briefly and then it is simply gone. No exception surfaces in the application, no job shows as failed, nothing lands anywhere a human looks. Members who should have been nudged just quietly were not. It surfaced as a product complaint rather than as an alert, which is the worst way for anything to surface: someone on the client's side noticed that members they expected to be nudged had not been. There was no count that disagreed with any other count, so there was nothing to reconcile. My first instinct was that the scheduling job was selecting the wrong members, and I spent real time verifying that query before accepting that the messages were being generated correctly and lost after publication. The diagnosis was the uncomfortable kind: the system was behaving exactly as built. Nothing was broken in the code. The architecture had no durable place for an undelivered notification to exist, so an undelivered notification did not exist. The fix was to put SQS between SNS and the consumers. SNS fans out to queues instead of directly to workers, the workers consume at their own pace, failed deliveries return to the queue and retry, and anything that keeps failing lands in a dead letter queue where it is visible, inspectable, and replayable. CloudWatch alarms on queue depth mean a stuck consumer now announces itself instead of hiding. What it changed for me is that I stopped treating durability as an optimisation. A queue gets its dead letter path before it gets traffic, and "what happens when this fails" is a design question, not an operations question. Silent success and silent failure look identical from the outside, which is why the failure path has to be designed on purpose. Implementing the client's architecture rather than redesigning it. The design was theirs; my leverage was execution speed and catching gaps, like the notification path above. The cost was living with structural choices I would have made differently. The benefit was momentum and a client who fully owned and understood their own system, which mattered more, because they keep it after I leave. Two async systems instead of one. Celery on Redis handles application background work; EventBridge with Lambda handles the scheduled re-engagement scan. Running both means two things to operate and monitor. Collapsing everything into one would have meant either dragging scheduled infra concerns into the app workers or moving app tasks out to Lambda, and each job sat more naturally where it landed. Integrity in the database, not just the serializer. Credits, entitlements and booking rules are enforced with database-level constraints and transactions rather than trusting the API layer. The cost is stricter migrations and less flexibility when requirements moved. The benefit is that no code path, including future ones nobody has written yet, can corrupt a balance. I would have built the notification path with the queue in it from the first day. The SNS-direct version was not a shortcut I took knowingly, it was a default I did not question, and questioning it would have cost an hour against the days it eventually took to find. I would also have separated two things I treated as one: accepting the client's architecture, and accepting its data model. Taking the architecture as given was the right call and I would make it again, but I extended that deference to structural decisions underneath it that were mine to question, and the trade-offs section above is honest that I then lived with choices I would have made differently. The failure-path review I now run before building a feature is the same instrument, and it works just as well pointed at a specification as at my own design.

Want to dig into this one?

I’m open to full-time backend, platform and data engineering roles, and happy to walk through any decision on this page in more detail than it deserves.
Get in touch