Every stack decision in a solo-founder SaaS has a cost. Not just money — time, complexity, and cognitive load. When I started building InspectIQ for the H0 Hackathon, I had one rule: every technology choice had to earn its place.
Here's what I chose and why.
Aurora PostgreSQL Serverless v2
The H0 Hackathon required an AWS database. I had three options: Aurora, Aurora DSQL, or DynamoDB.
I chose Aurora PostgreSQL Serverless v2 for one reason that trumped everything else: Row Level Security.
InspectIQ is multi-tenant from day one. Every inspector is a separate tenant with completely isolated data. RLS lets me enforce that isolation at the database layer, not the application layer. One missing WHERE clause in application code can't leak data between tenants when the database itself enforces the policy.
Aurora Serverless v2 also scales to near-zero between requests. For an early-stage SaaS with unpredictable traffic, that matters. I'm not paying for a database that sits idle at 2am.
The cold start is subsecond. I've never noticed it in production.
AWS App Runner
I needed a place to run FastAPI. The options were Lambda, ECS, EC2, or App Runner.
Lambda would have required restructuring the FastAPI app into a Lambda handler.
Doable, but extra work for no gain at this stage.
ECS gives you more control but requires more setup — load balancers, task definitions, service discovery. I didn't need that control yet.
App Runner takes a container image from ECR and runs it. Auto-scaling, health checks, HTTPS, all handled. I push to ECR, App Runner deploys. That's it.
For a solo founder building fast, App Runner is the right call.
Vercel + Next.js 14
The frontend needed to be fast to build and fast to load on mobile. Inspectors use their phones in the field, sometimes with marginal 4G connections.
Next.js App Router gives me Server Components for the initial data fetch (fast, no client-side waterfall) and Client Components for the real-time parts, the findings autosave, the photo upload progress, the observation condition buttons.
The autosave debounces 800ms after the last keystroke. The inspector types a finding, stops typing, and it saves automatically. No save button. No lost data if they close the app.
Vercel deploys on every push to main. Preview deployments for every PR. Zero infrastructure management on the frontend side.
AWS S3 for photos
Home inspectors take a lot of photos. Every defect gets documented. A full inspection might have 20-40 photos.
The naive approach: upload photos to the FastAPI backend, which forwards them to S3. That works, but it doubles the bandwidth and adds latency, the photo travels from the phone to App Runner to S3 instead of phone to S3 directly.
The right approach: presigned PUT URLs. The backend generates a temporary URL that lets the phone upload directly to S3. The file never touches the backend.
The path is:
tenants/{tenant_id}/inspections/{inspection_id}/findings/{finding_id}/{uuid}.jpg
Tenant isolation in the S3 key structure. The backend only stores the key and a presigned GET URL (24h expiry) for serving the photo in the UI and PDF.
WeasyPrint for PDF generation
Generating PDFs is harder than it looks. The options I considered:
- Headless Chrome (Puppeteer): Works great, but adds a Chrome binary to the Docker image. That's 300MB+ and a security surface I didn't want.
- ReportLab: Python library, but building complex layouts with photos and branding in ReportLab feels like writing assembly code.
- WeasyPrint: Renders HTML/CSS to PDF server-side. I write the report as a Jinja2 HTML template, WeasyPrint converts it. The template is readable, maintainable, and easy to update.
WeasyPrint has quirks — it doesn't support flexbox in table cells, and object-fit doesn't work. But once you know the constraints, it's predictable. A 15-section inspection report with photos renders in under 3 seconds.
AWS Cognito
Auth is not where I wanted to spend time. Cognito handles JWT issuance, token refresh, user management, and MFA if I ever need it. I store a custom tenant_id claim in the token so the backend knows which tenant is making the request without a database lookup on every call.
The inspector's inspector_id is resolved on the backend from the JWT sub via a lookup in inspector_profiles. The frontend never sends database IDs — only business data. This was an architecture decision I got right after initially getting it wrong.
Terraform for infrastructure
Everything is in code. The Aurora cluster, App Runner service, S3 bucket, Cognito user pool, ECR repository, security groups, VPC, all Terraform in a separate mcag-h0-infra repo.
When something breaks, I know exactly what's deployed. When I need to
replicate the environment, I run terraform apply.
What I'd change
Honestly, not much at this stage. The stack is boring in the best possible way, proven technologies that do what they say.
The one thing I'd add sooner: CloudFront in front of S3. Right now I'm using presigned GET URLs with 24h expiry for photos in the PDF. That creates a race condition if the PDF is generated close to the URL expiry. CloudFront with Origin Access Control gives permanent URLs that WeasyPrint can always reach.
That's on the roadmap for July.
The live product
InspectIQ is running in production at mcag-h0.vercel.app.
First customer confirmed at $99/month starting July 2026.
The stack made it possible to build a production-grade multi-tenant SaaS in the H0 Hackathon window. Not a demo. A product.
I created this content for the purposes of entering the H0: Hack the Zero Stack Hackathon by AWS and Vercel. #H0Hackathon
Top comments (0)