Running in production 2026
Cinehal
A movie recommender shaped like a card deck that learns your taste from tags. A university team project I later moved from one EC2 box to Lambda, S3 and CloudFront, with real authentication.
- 156
- backend tests
- cents / month
- steady-state cost
The product
One movie at a time, four actions: already watched, want to watch, don’t want to watch and undo. Each choice adds points to the movie’s tags, and that running profile ranks what comes next. Tag filters, personal lists and achievements (“Que drama!” after five dramas) sit around it.
The product was built in a single semester by a team at AGES, PUCRS’s software engineering agency. Everything below is what I added afterwards, on my own.
Moving to serverless
flowchart TD
U([Browser]) -->|HTTPS| CF[CloudFront]
CF -->|/| S3[(private S3<br/>built SPA)]
CF -->|/api/*| FU[Lambda Function URL]
FU --> L[Lambda<br/>Express + TypeORM]
L -->|TLS| N[(Neon<br/>Postgres)]
L --> SSM[SSM Parameter Store]
U -.->|id_token| G[Google Identity]
L -.->|verifies against JWKS| G
The original deployment was one EC2 instance running nginx, the API and Postgres, billed by the hour even when idle. Now:
- the built SPA lives in a private S3 bucket, served through CloudFront with Origin Access Control;
- Express is wrapped by
serverless-httpin a Lambda, reached through a Function URL that only CloudFront uses; - Postgres moved to Neon, with TLS against a verified certificate;
- SPA and API share one domain, so there is no CORS at all.
The handler builds the app outside the invocation to reuse warm containers, and rechecks the database connection on every call: Neon’s pooler closes idle connections, and a container can come back warm holding a dead one.
Authentication
What had replaced Firebase verified nothing: the token was base64 JSON built by the client, so sending someone’s id returned their data. Signing on the frontend wouldn’t help, since the key would ship in the bundle. Now:
- Google Identity Services hands an
id_tokento the browser; - the backend verifies it against Google’s JWKS, pinning
audienceandissuer; - the backend issues its own 8-hour session in an
HttpOnly; Secure; SameSite=Laxcookie.
Secrets
Secrets live in SSM Parameter Store as SecureString, read once at cold start. Lambda
environment variables were ruled out on purpose: lambda:GetFunctionConfiguration is part of the
managed ReadOnlyAccess policy, so anyone with read-only access to the account could read them.