Every system inside aimlcompanion.ai in production
A case study of one specific product, not a universal template, built by me over the last 5+ months (including content curation) and still iterating. The full stack running aimlcompanion.ai today, plus the larger-scale upgrades I have not made. Then an honest take on which of those upgrades would be worth it for this product and which would only slow shipping.
A case study of aimlcompanion.ai specifically, not a template for every production app. The visible product of a content-heavy learning platform is the modules, the visualisations, the narrated walkthroughs, the ML mini-games, and the progress analytics. The invisible product is the production stack wrapping all of that. Caching, auth, deploys, observability, and the layered safeguards that turn a working app into a service real users keep paying for. This post walks through every one of those layers as it runs behind one live AI and ML learning app. It takes a clear-eyed look at where larger-scale engineering would add value and where it would only slow shipping.
Why this post exists
The visible product of a content-heavy learning platform is what people come for. That means the modules, the visualisations, the narrated walkthroughs in three languages, deep dive insights, the code examples, real world projects, the ML mini-games, and the per-learner progress analytics.
The invisible product is the production stack wrapping all of that. The caching that keeps it fast. The auth that powers every learner identity, free and paid, along with their progress, bookmarks, and tier-aware access. The deploy pipeline that ships content updates without breaking live sessions. The observability that catches problems before users do, and the layered safeguards that turn a working app into a service real users keep paying for.
This post is a high level tour of the important systems running in production behind one specific live AI and ML learning platform, aimlcompanion.ai. Built and shipped solo, drawing on 16+ years of production engineering experience on systems that have processed billions of telemetry events per hour. The last several years have been focused on traditional AI/ML and, more recently, on GenAI and Agentic AI. The catalogue today spans tens of tracks, hundreds of modules, thousands of narration audio files across three languages, and hundreds of prerendered SEO routes.
At each layer I also call out, honestly, what a larger-scale version of that same layer would look like, and which of those upgrades would be premature for this product at this stage. Those larger-scale upgrades will be made as and when the user base grows past the threshold that actually justifies each one, not earlier as a default. If you are building anything past a demo, the post is a map of one set of trade-offs, not a checklist that applies to every app.
How to read the diagram
- Edge support (top strip): The three AWS services that make HTTPS work attach to a single CloudFront distribution. Route 53 holds the apex DNS (.ai and .com ALIAS records), ACM issues TLS certificates in us-east-1 and the regional certificate for the origin, and AWS Shield provides DDoS protection. CloudFront itself runs a CloudFront Function at the edge that does the .com to .ai 301 redirect and rewrites SPA paths to the prerendered HTML files.
- Request path (middle): A learner request enters CloudFront and branches one of two ways. Static and SEO traffic goes to S3, which serves the prerendered HTML, immutable hashed assets, and signed video URLs gated by a CloudFront key group. Authenticated traffic and the API go to the EC2 application server, running Node and PM2 in cluster mode behind nginx. Express, OAuth, payments, CSP nonces, CSRF, rate limiting, IP hashing, and Vary headers all sit on top.
- Data services (right): EC2 reads sessions and pub/sub from Redis with a cron heartbeat channel and graceful degradation if Redis becomes unreachable. Relational data lives in managed RDS PostgreSQL with automated backups and migrations applied on every deploy.
- Reliability (grey pills): EC2 sits behind an Application Load Balancer across two availability zones, and Redis runs on ElastiCache with multi-AZ replication. Both close the only structural single-point-of-failure that mapped to a real customer-facing risk at this scale.
- Ops, observability, deploy (bottom strip): GitHub Actions runs the deploy pipeline in several stages. Lint, test, build, stage on EC2 alongside the live release for a blue-green flip, migrate, reload in cluster mode, run a health check with auto-rollback, then sync S3 and invalidate CloudFront. SSM Parameter Store holds secrets accessed via IAM roles. CloudWatch collects Pino logs, OpenTelemetry traces, metrics, alarms, and the cron heartbeat. AWS SES handles outbound transactional and broadcast email from EC2. A separate card flags the rate-limit and anti-scrape defences that sit on every content endpoint. A small sidebar lists the three deploy targets (EC2, S3, CloudFront) so the diagram stays free of crossing arrows.
This is a case study, not a universal template. Every architectural decision below is a trade-off that was right for aimlcompanion.ai given its current scale, content-heavy shape, and solo-engineer constraint. A B2B SaaS with enterprise compliance needs, a high-frequency trading platform, or a consumer social app would each make different calls at almost every layer. Read it as one product explaining the choices it made and why, not as how every production app should be built.
The stack is replaceable. The content compounds. Read the rest of the post with that frame in mind. Each architectural decision below was chosen deliberately. Most came directly from production incidents that taught me why the easy default would have been wrong. Together they form a stack that ships content fast without breaking what is already live.
Infrastructure
The stack is AWS, kept deliberately boring.
- EC2 for the application server, running Node, Redis, nginx, and PM2
- RDS for PostgreSQL (managed, with automated backups)
- S3 for static asset hosting
- CloudFront in front of everything
- Route 53 for DNS, with ALIAS records at the apex
- SSM Parameter Store for secrets
- IAM with least-privilege roles for the build pipeline and the application server
The whole production footprint costs almost nothing per month, and that is the point. Boring infrastructure is a feature when one person owns every layer.
The current setup already runs multi-AZ within a single region, EC2 behind an ALB across two availability zones, RDS multi-AZ, and ElastiCache multi-AZ replication for Redis. The next genuine jump is multi-region, not multi-AZ, and that brings a different shape of work. Multi-region active-active so a regional AWS outage does not take the site down. Aurora with cross-region read replicas instead of standard RDS. Transit gateways and dedicated VPC peering when multiple services need to share data privately. Private subnets behind NAT gateways for tightened egress control. Each is correct at higher scale and wrong for a current scale product today. Most of them introduce more failure modes than they remove until traffic genuinely justifies the complexity.
Frontend
The app is React plus Vite. It is also a PWA, installable on mobile and desktop, with full offline behaviour for previously visited content.
What goes into making that actually work in production
- Route-level code splitting via React.lazy
- A Service Worker (Workbox) with six runtime caches, each with strict status filters so a single 404 cannot get cached for 90 days (the classic Service Worker bug)
- Self-hosted fonts so the app does not depend on Google Fonts at runtime
- A theme system with CSS variables for dark and light modes
- Responsive design from 320px mobile to 1920px desktop with clamp() for fluid typography
- localStorage-backed persistence for theme choice, anonymous progress before sign-in, bookmark drafts, recently viewed modules, and the 30-second guard that prevents chunk-recovery reload loops, so a returning learner never starts fresh
- Debounced inputs across search, settings, and any other typing flow so the backend receives one request per pause instead of one per keystroke
The seven-layer stale chunk recovery
The most defensive piece of the frontend is a layered stale-chunk recovery system that took multiple production incidents to fully build out. The problem looks like this. A deploy ships, the user's browser still has the old index.html cached, and that cached HTML points at chunk filenames that no longer exist on the server. Without recovery, the user lands on a blank error page and the only way out is for them to hard-refresh, which most users never do.
The seven layers cover every place a chunk load can fail. They include React.lazy wrappers, the ErrorBoundary, the Service Worker controller-change handler, and an inline script in index.html that fires before React itself loads. All seven share a 30-second guard in sessionStorage so the recovery cannot loop into an infinite reload if something deeper is broken.
A larger-scale version of the frontend goes further with server-side rendering at the edge, React Server Components, streaming HTML, and an islands architecture for partial hydration. Each is a real upgrade at scale, and each adds operational complexity that a solo team cannot reasonably maintain alongside content production and customer support.
SEO without a CMS
Single Page Apps are notoriously bad at SEO. Google crawls them inconsistently, and social previews are usually broken because the page returns an empty shell before JavaScript runs.
The app sidesteps that by generating fully prerendered HTML for every public route at build time. Every route gets a unique title tag, a unique meta description, a canonical URL, Open Graph tags for social sharing, and a sitemap.xml entry. The total build artefact is small enough to live entirely in S3.
A CloudFront Function at the CDN edge rewrites SPA paths to those prerendered files so Google sees real HTML, not an empty shell. Auth-only pages fall back to the SPA shell instead, so private content never gets prerendered into a publicly cacheable file.
Here is the small detail that makes this trickier than it looks. CloudFront has a built-in option called custom error responses that lets you send any not-found URL to /index.html instead. On the surface, that is exactly what the SPA rewrite needs to do. The problem is that this option applies to every URL going through CloudFront, with no way to scope it to just the public pages. The backend uses similar error codes when, for example, a free user tries to open premium content, and the same setting would intercept those too. The user would land on /index.html as if the page did not exist, and the paywall would silently break. CloudFront Functions are different. Each one is attached to a specific URL pattern, so the SPA rewrite only fires on the public pages and the backend stays untouched. Both options can do the rewrite. Only one of them does it without accidentally breaking the rest of the site.
At scale this gets more elaborate, adding ISR (incremental static regeneration), on-demand revalidation, hreflang for international SEO, automated structured data generation, and a dedicated SEO team. Most of that is unnecessary if your content changes on the order of weeks, not minutes.
Caching, layered from edge to database
Caching is the part of the stack I enjoy most because every layer here was added in response to something that actually broke in production. The order below is the standard one, starting at the edge (closest to the user) and moving inward toward the database. Four of the six are true caches in the textbook sense. They store the result of expensive work so the next request can skip that work. Those four are the CDN at the edge, the Service Worker running in the browser, a short-lived in-process cache on the server, and a hashed lookup table in PostgreSQL.
The other two rows are not caches by themselves. Redis stores active sessions, not derived data, so it behaves like a cache but is really a session store. And per-file content hashing is a naming trick that puts a hash in every asset URL, which is what makes the top two caches safe to cache aggressively across deploys. Both rows are listed because the caching story would be incomplete without them.
| Layer | Where it lives | What it caches | Invalidation |
|---|---|---|---|
| 1. CDN edge | CloudFront | index.html, static assets, signed video URLs | Path-specific behaviours, invalidation on deploy |
| 2. Service Worker | Browser (Workbox) | Pages, JS, data, images, CSS, audio | NetworkFirst with timeout, status-filtered, cleared on SW update |
| 3. Redis | Application server | Sessions only | Ephemeral, graceful degrade if down |
| 4. PostgreSQL content cache | Database | ELI5 simplifications | Auto-invalidate by MD5(source text) |
| 5. In-memory TTL | Express process | Premium access checks | 30 second TTL |
| 6. Per-file content hashing | Build output | Narration audio files | Hash changes only if file content changes |
Why caching at every layer pays
CloudFront has six path-specific behaviours. index.html sits behind a short cache. /api/* and /auth/* have caching disabled because they are authenticated, and /assets/* has caching disabled because premium chunks are auth-gated by Express. /sw.js gets its own behaviour to bypass the 24-hour legacy default that traps most teams on their first PWA, and /videos/premium/* uses signed URLs with a one-year cache. A CloudFront Function at the edge handles domain redirects and SPA rewrites with zero round-trip to the origin.
The Service Worker has multiple runtime caches split by content type (pages, JS, data, images, CSS, audio). Every cache uses cacheableResponse with a 200 status filter so error responses never persist. The audio cache is the largest and longest-lived. The pages cache uses NetworkFirst with a short timeout, so the user always gets fresh HTML when the network is up and falls back to cache only when the network is slow or offline.
Redis stores sessions only. When Redis goes down, the app degrades gracefully via a connection flag. Public content keeps working, authenticated requests return 401, and the health endpoint returns 200 with a degraded status so the deploy pipeline does not block on a Redis outage.
The PostgreSQL content cache stores ELI5 simplifications keyed by a content hash of the source insight text. Editing the source insight changes the hash, which invalidates the cache automatically with no manual purge step. The same pattern shows up one more time, just in memory. A short-lived in-process cache on premium-access checks means the database is not queried on every ELI5 request, which adds up quickly when one learner browses through many modules in a session.
The audio layer is the most operationally significant one. Each MP3 carries a content hash in its URL, so only changed files re-download after a deploy. Before this, a global build version invalidated every audio URL on every release, which meant learners on slow connections would re-download hundreds of megabytes of identical narration after a minor frontend update.
A larger-scale version of this caching stack looks different. Lambda@Edge routes free and premium chunks separately from a single distribution, so global users do not pay the round-trip latency to a single region. Multi-region cache replication keeps reads from crossing continents, and prefetching gets driven by ML. All of it is correct at higher scale, and the wrong place to spend a solo engineer's month at this one.
Payments
Razorpay is the primary gateway and handles both INR and USD, so a single account covers domestic and international transactions without standing up a second processor.
What sits around the gateway
- Webhook signature verification on every event
- Webhook deduplication keyed on the gateway's event ID with a composite fallback, persisted so a duplicate retry (the gateway's default behaviour) never double-charges, double-activates, or double-refunds a user
- A refund event log that captures every state change for audit
- Refund cooldown windows to detect abuse patterns
- Plan switching with proration logic
- Founding-member and lifetime tier handling separate from recurring subscriptions
At higher transaction volumes this becomes a primary gateway plus a hot secondary with automated failover, an automated chargeback dispute workflow, and ML fraud scoring on transaction patterns. All useful past a certain volume, and all overkill at the scale of a focused single-product company.
Security and compliance
Security posture
- Content Security Policy with per-request nonces for inline scripts (not a static CSP)
- CSRF tokens bound to the session, not to the user
- Session regeneration on every authentication state change
- HTTPS enforcement at the CDN edge with HSTS preload
- Cookie Vary headers on authenticated responses to prevent cross-user cache contamination
- Rate limiting at the Express layer with tiered limits per route category, tightest on auth and premium endpoints
- Anti-scraping detection that tracks how many distinct resources a single IP requests in a rolling window. Aggressive crawlers above a tuned threshold get blocked, stacked on top of the rate limiter for defence in depth
- OAuth with Google and GitHub, with auto-linking by email so users do not get duplicate accounts
- AWS Shield on the CloudFront distribution
- And more .....
Compliance
GDPR consent is gated to EU, EEA, and UK users via geolocation, not shown globally. The privacy policy and terms of service were rewritten from scratch rather than copied from a template. IP addresses are hashed at the application layer, so raw IPs are never stored. Retention cleanup jobs run on a schedule. A documented DMCA process and a breach runbook live in the same playbook directory as the deploy pipeline.
DPDPA (India's data protection law) support is implemented and waiting for a one-line flag flip when the rules are formally notified. CCPA / CPRA compliance for California residents is also in place. The footer carries a Your Privacy Choices link, the privacy policy includes the California-specific disclosures the law requires, and right-to-access and right-to-delete requests route through the same admin tooling that serves GDPR requests.
An enterprise-grade posture adds SOC 2 Type II, ISO 27001, a dedicated Data Protection Officer, formal Records of Processing Activities, vendor risk management, an external red team program, and a Security Operations Center. That posture is necessary for enterprise sales and overkill for consumer subscriptions today.
Inside every learning module
Every learning module is a layered package, not a single block of text. Most layers are hand-curated content. A small AI-assisted layer handles simplification and narration at content-edit time, never on the request path.
- Core insight: the foundational explanation of the concept, hand-written and curated by me
- Concept overview visualisation: an animated SVG diagram that introduces the concept visually, narrated alongside the visual so the learner sees and hears the idea at the same time
- Interactive visualisations: clickable, draggable, parameter-tuning visualisations where the learner manipulates variables and watches the model respond in real time
- Code per module: runnable code examples that show the concept in a real implementation, often paired directly with the visualisation above so the math, the picture, and the code line up
- Quizzes: multi-question knowledge checks at the end of each module so a learner can verify they actually understood the concept, not just consumed it
- Real-world project: where applicable, a full project walkthrough that applies the concepts in a portfolio-worthy implementation rather than a toy notebook
- ELI5 simplification: generated once via a frontier LLM, cached in PostgreSQL keyed by a content hash of the source insight text, and auto-invalidated on every edit so the simplification never drifts from the source
- Voice narration in three languages: English, Hindi (technical terms left in English), and Spanish (technical terms left in English)
The narration pipeline uses Google Cloud TTS. The audio files are versioned by content hash so the Service Worker can cache them aggressively while still serving updates when the underlying script changes.
The bottleneck in this pipeline is never compute or cost. It is the quality work that turns each layer into something a learner actually understands. The audit step that catches narration drifting from the visual it is supposed to teach. The iteration on an interactive visualisation until a parameter slider becomes a moment of insight. The code example that gets shortened until it actually teaches the point. The quiz question that gets rewritten until it tests understanding instead of recall. Generating thousands of robotic narrations is fast. Producing the educator-quality version of every layer for every module is months of careful work, and that gap is most of the actual product.
Past a certain scale this gets richer. Streaming TTS for interactive responses, voice cloning for personalised narrators, and real-time adaptive difficulty driven by engagement signals. Then multi-modal content generation pipelines that draft visualisations and code examples for human review. The last two are genuinely interesting for the long term, the first two are mostly cosmetic at this stage.
Observability, email, reliability, and deploys
Observability
Pino emits structured logs that flow into CloudWatch alongside metrics and alarms. OpenTelemetry instrumentation produces distributed traces across the full request path. A slow page traces back to the specific database query, cache miss, or downstream call that caused it, instead of being guessed at from logs alone. A single health endpoint probes both PostgreSQL and Redis on each invocation, and a separate cron heartbeat endpoint catches scheduled jobs that miss their window. The client ships its own error reporter that posts to the same backend, slow query logs are captured at the database, and PM2 monitors the cluster of Node processes.
Transactional and broadcast email runs through AWS SES over SMTP via nodemailer from EC2. Two verified senders sit behind the application, one for transactional sends (welcome, receipts, account events) and one for broadcast sends, both on the verified domain. The transactional send fires asynchronously after a successful payment and is wrapped in try/catch so a failure never blocks the upgrade flow itself. SES is in production mode with quota well above current send volume. Inbound replies are forwarded through a third-party inbox forwarder into a single mailbox for support handling.
Reliability patterns under the hood
A few patterns show up across the production code wherever they pay off. Idempotency and deduplication on side-effecting paths, so a duplicate payment-gateway webhook (their default retry behaviour) never double-grants premium access or double-issues a refund. The dedup key is the gateway's event ID with a composite fallback, persisted so the request consults it before doing any work.
Circuit breaker plus retry with exponential backoff on external API calls, so realtime features keep their live feeds alive when an upstream source flaps. A small number of retries on 429 and 5xx responses with increasing delays, then the circuit opens and the channel publishes a degraded status to connected clients instead of hammering the upstream. Postgres advisory locks on the critical state-mutation paths so two concurrent webhooks for the same user serialise rather than racing each other into inconsistent state.
Deploys
Deploys run through GitHub Actions using a blue-green strategy on EC2. The pipeline lints, tests, and builds. It stages the new release in a parallel directory on the server, the green build sitting alongside the live blue build, and runs migrations. Then an atomic symlink flip from blue to green, a PM2 reload in cluster mode for zero downtime, and a health check. If anything fails after the flip, the symlink swaps back to the previous (blue) build and PM2 reloads again, so the user sees no downtime and the bad release is gone within seconds. Only after a successful flip does S3 sync the new static assets and CloudFront invalidate.
At higher scale this becomes formal SLO dashboards with alerting, and chaos engineering as a standing practice. Progressive rollouts use feature flags as deploy gates, and canary deployments route a small percentage of traffic to the new version before a full switchover. Each is a real upgrade at the right traffic level.
What larger-scale engineering adds
Putting the per-section gaps together, here is what larger-scale production engineering adds.
- Multi-region everything for resilience and latency
- A team that owns each layer (SRE, security, platform, data)
- Formal certifications (SOC 2, ISO 27001) for enterprise customers
- Edge compute for personalisation and chunk routing
- ML-driven observability and fraud detection
- 24/7 on-call coverage with NOC support
- Chaos engineering and red team programs
Every one of those is the right answer at a certain scale, and not one of them is the right answer at the stage of a focused single-product company today. Building them prematurely slows shipping without improving outcomes, and most of them quietly add new failure modes that have to be staffed against.
Premature scale-out architecture is the most common failure mode I see in engineering-led startups. Founders read a published architecture post from a large engineering org and implement the multi-region, event-sourced, microservice version of what should be a monolith on one box. The customer never sees the engineering; they only see how long it took to ship.
How the system handles its Non-functional requirements (NFRs)
For readers who skim instead of read, here is the same architecture compressed into a single Non-functional requirements (NFRs) view. Each row is one sentence and points back to the section in the post where the topic is covered in depth.
| Non-functional requirement | How the system handles it today |
|---|---|
| Availability | Single region with multi-AZ for app, cache, and database. EC2 behind an ALB across two availability zones. ElastiCache and RDS both multi-AZ. Health check with auto-rollback on every deploy. |
| Performance | Caching layered from edge to database (CDN, browser Service Worker, in-process TTL, content-hash memoisation in PostgreSQL). Brotli plus gzip compression. Prerendered HTML for every public route. Service Worker for offline reads. |
| Scalability | Horizontal scaling on EC2 via PM2 cluster mode behind the ALB. Stateless application instances. Multi-region active-active is the next jump and is deferred until traffic justifies the cost. |
| Security | CSP with per-request nonces, CSRF tokens bound to the session, and session regeneration on auth state changes. HSTS, OAuth with auto-link, tiered rate limiting per route, anti-scraping detection on content endpoints, IP hashing, and AWS Shield on the CDN. |
| Compliance | GDPR consent gated by geolocation (EU, EEA, UK only). CCPA / CPRA Your Privacy Choices footer link with California-specific disclosures in the privacy policy. DPDPA-ready with a one-flag flip. Hand-written privacy policy and terms of service. Retention cleanup jobs on a schedule. Documented DMCA process and breach runbook. |
| Observability | Pino structured logs and OpenTelemetry distributed traces in CloudWatch. A health endpoint that probes PostgreSQL and Redis. A cron heartbeat endpoint that catches jobs missing their window. Slow query logs at the database. Client-side error reporter. |
| Reliability patterns | Idempotency on every side-effecting path. Webhook deduplication keyed on the gateway's event ID. Circuit breaker plus exponential backoff retry on external API calls. Postgres advisory locks on critical state-mutation paths. Graceful degradation when Redis is unreachable. |
| Maintainability | Blue-green deploy pipeline with lint, test, atomic flip, health gate, and auto-rollback. Versioned database migrations applied on every deploy. Secrets in SSM Parameter Store. Boring, well-understood AWS services across the stack. |
| Disaster recovery | RDS automated daily backups with point-in-time recovery. Stateless EC2 instances, so failure recovery is booting another one. Multi-AZ replication for Redis and the database. CDN keeps serving the previous build if a health check fails after deploy. |
| Cost efficiency | Single AWS account on a small EC2 instance with managed services only where they actually save engineering hours. The monthly bill is negligible compared to the engineering hours it saves. |
The table is a summary, not a substitute. Every row above hides trade-offs and incident-driven decisions that the post documents in depth. For a working understanding of why the stack looks like this, the narrative sections are still the answer. The table is for the skimmer and for the reader returning months later who needs to find a specific dimension fast.
What actually matters
If you are building anything past a tutorial app, the lesson is the same. Spend just enough on infrastructure to not embarrass yourself, and spend everything else on the thing that compounds.
Every architectural choice in this post is downstream of that principle. The six caching layers exist because shipping content updates without breaking the deployed app is worth defending in code. Chunk recovery has seven layers because the alternative is scheduling downtime windows, which fragments writing time. The AWS stack stays deliberately boring because every hour spent on exotic infrastructure is an hour not spent writing modules.