The A Million Pixels Tech Stack

(Another repost from the A Million Pixels blog.)

In my first post, I talked about why I built A Million Pixels. As I mentioned here, one of the most common questions I got after the launch was: what stack is this built on and who made those decisions? This post answers those questions.

Some context: The idea was mine, with no AI involved in the equation. The overwhelming majority of the code was written with agentic coding tools (primarily Claude Code and Codex). The stack reflects decisions made solely by me, mostly by the agent, or collaboratively. That last bit feels a little weird to write. I can cover the agentic workflow in detail in a future post if it's interesting to readers.

The Life of a Pixel

Here's a simplified view of the architecture:

                         ┌─────────────┐
                         │   Browser   │
                         │  (PixiJS)   │
                         └──────┬──────┘
                                │
                         ┌──────▼──────┐
                         │   Vercel    │
                         │  Next.js 16 │
                         └──┬───┬───┬──┘
                            │   │   │
              ┌─────────────┤   │   ├─────────────┐
              │             │   │   │             │
       ┌──────▼──────┐ ┌───▼───▼───▼───┐ ┌───────▼──────┐
       │   Stripe    │ │   Supabase    │ │  Replicate   │
       │  Checkout   │ │  (Postgres)   │ │  (AI Images) │
       └─────────────┘ └───────────────┘ └──────────────┘
                            │   │   │
              ┌─────────────┤   │   ├─────────────┐
              │             │   │   │             │
       ┌──────▼──────┐ ┌───▼───▼───┐  ┌──────────▼──┐
       │  Upstash    │ │  OpenAI   │  │ Cloudflare  │
       │   Redis     │ │ Moderation│  │     R2      │
       └─────────────┘ └───────────┘  └─────────────┘

Payment → generation → moderation → storage → canvas. That's the core loop. Everything else supports it.

The Stack at a Glance

Technology What It Does
Vercel Hosting and deployment
Next.js 16 + React 19 App framework and UI
TypeScript Type safety across the entire codebase
Tailwind CSS 4 + shadcn/ui Styling and component library
Supabase (PostgreSQL) Database with row-level security
Upstash Redis Caching and rate limiting
Stripe Checkout Payment processing
Replicate (SDXL, Flux 2 Pro) AI image generation
OpenAI Text moderation and embeddings
Sightengine Image moderation
Google Web Risk URL safety checking
Cloudflare R2 Object storage for generated images
Resend + React Email Transactional email
PixiJS 8 Canvas rendering
BotId + Cloudflare Turnstile Bot protection
Supabase Auth (GitHub OAuth) User authentication
Datadog + OpenTelemetry Observability (APM, RUM, logs)
GitHub Actions CI/CD pipeline
MDX (next-mdx-remote) This blog

Now let's dig into each one.

Hosting & Deployment

Vercel

This is the first decision I made, as it impacted so many other decisions. I considered Railway, Render and Vercel. They all looked compelling. In the end, Render and Railway looked more like the infra platforms I've used in the past, so I went with Vercel. I've since delpoyed apps on both Railway and Render, and both are solid. You should decide based on the strengths of each platform vs your needs (ie. Choose Vercel you're building a frontend-heavy site or using Next.js. Choose Railway if you need a fast, simple backend deployment and quick DB setup. Choose Render if you have a traditional full-stack app requiring a persistent server and want predictable costs.)

Vercel is the company behind Next.js, and the deployment experience reflects that. Push to a branch, get a preview deployment. Merge to main, get a production deployment. The developer experience really is great. Serverless functions, edge middleware, image optimization, and cron jobs are all built in.

Alternatives considered: Railway, Render

Core Framework

Next.js 16, React 19, TypeScript, Tailwind CSS 4, shadcn/ui

Next.js was the obvious choice for a project like this once I decided on Vercel. Server Components by default, the App Router, API routes, and middleware all in one framework. React 19 brings Server Actions and improved streaming, which simplify a lot of the data flow.

TypeScript was a collaborative choice, although one of the the Agents said using it "is non-negotiable for anything beyond a toy project. When you're collaborating with AI agents that generate a lot of code, type safety acts as a persistent guardrail. The compiler catches mistakes that neither you nor the agent noticed." What's interesting about this is that until I added an explict line about types to my CLAUDE.md and AGENTS.md, they constantly used "any".

Tailwind CSS 4 paired with shadcn/ui, was completely chosen by the Agents.

Alternatives considered: Remix, SvelteKit. Both are fine options, but once I decided on Vercel a lot of this just fell into place.

Database

Supabase (PostgreSQL with Row-Level Security)

Supabase gives you a managed Postgres database with a great developer experience: auto-generated REST APIs via PostgREST, real-time subscriptions, and built-in auth. I use it as a straightforward database — no ORMs, just the Supabase client with typed queries.

Row-Level Security (RLS) means the database itself enforces access rules. Even if you screw up an API route, the database won't return data the caller shouldn't see. It's defense in depth, and it's one of Supabase's strongest selling points.

Migrations are forward-only and backward-compatible by design. Every schema change has to work with both the current and previous code version, because code deploys can roll back but migrations can't. This is a discipline worth adopting early. Migrations are automatically handled by the CI/CD pipeline.

Alternatives considered: PlanetScale, Neon, raw Postgres. I've been looking for an excuse to trying Supabase in earnest and this was it.

Cache

Upstash Redis

Upstash provides serverless Redis, and has native Vercel integration. It's used for rate limiting, caching (including AI embedding results), and cross-instance coordination for things like circuit breaker state.

The per-request pricing model means you're not paying for idle connections. For a project with variable traffic, this matters more than you'd think.

Alternatives considered: Vercel KV (which is actually Upstash under the hood), Momento, DynamoDB. Upstash directly gives you the most control and the best pricing transparency. The Vercel 1 click made the decision for me.

Payments

Stripe Checkout

Stripe Checkout is a hosted payment page that handles the entire purchase flow. You redirect the user to Stripe, they pay, and you get a webhook when it's done.

For a project like this where the purchase flow is straightforward (select a block, pay $1/pixel, get an image), Checkout is ideal. It's PCI compliant by default, handles international payment methods, and the webhook model makes the backend integration clean and idempotent.

The Stripe API version is pinned to match the SDK's type definitions. This is a small thing, but it prevents subtle runtime bugs from API version drift.

Alternatives considered: I just chose Stripe, it's what I know. I works. It has great docs.

AI Image Generation

Replicate (SDXL, Flux 2 Pro)

This is the core of the product. A user writes a prompt, and an AI model generates a unique image for their block on the canvas.

I use Replicate as the inference provider because it gives you access to multiple models through a single API. The site currently supports two models — SDXL and Flux 2 Pro — with model selection driven by feature flags. This lets me A/B test different models and roll out new ones without code changes.

The generation flow is asynchronous: the API kicks off a prediction, and Replicate sends a webhook when it's complete. This decouples the user-facing request from the potentially slow generation step.

Alternatives considered: Running models directly on GPU instances (too expensive and complex to manage), DALL-E (good but less model flexibility), Midjourney (no API at the time I started building). This was a collaborative choice, as it was not an area I had much experience.

AI Embeddings

OpenAI (text-embedding-3-small)

Every prompt submitted to the site gets an embedding — a 1536-dimensional vector that captures the semantic meaning of the text. This powers semantic search and content discovery across blocks.

I chose text-embedding-3-small for the balance of quality and cost. Embeddings are cached in Redis with a content-addressed key (SHA256 of normalized text), so duplicate or near-duplicate prompts don't generate redundant API calls.

Alternatives considered: Cohere Embed, Voyage AI, open-source models via Hugging Face. I was already using OpenAI for moderation, so it didn't seem worth adding another service to the equation for this.

Content Moderation

OpenAI Moderation + Sightengine + Google Web Risk

Moderation is a three-layer system, because no single service catches everything, and I've learned the importance of automated moderation the hardway with LinuxQuestions.org.

  1. Text moderation — OpenAI's moderation endpoint analyzes every prompt before image generation begins. It's fast, cheap, and catches the obvious stuff.
  2. Image moderation — After an image is generated, Sightengine runs computer vision analysis across multiple categories (nudity, violence, etc.). Text moderation alone isn't enough because the same prompt can produce very different images depending on the model and seed.
  3. URL safety — Any user-submitted URLs are checked against Google Web Risk to prevent phishing and malware links.

On top of these, there's a pattern-based detection layer for common jailbreak and prompt injection attempts. When detected, these route to manual review rather than auto-rejection — defense in depth without punishing false positives.

Each external moderation service has its own circuit breaker with exponential backoff. When a service degrades, new requests route to manual review instead of blocking users or silently skipping moderation. Circuit breaker state is persisted in Redis so it's coordinated across all serverless instances. I probably went overboard here, but again... lessons learned.

Alternatives considered: AWS Rekognition, Azure Content Safety, building everything with OpenAI's vision models. The multi-provider approach adds complexity but significantly reduces the risk of any single point of failure in moderation.

Object Storage

Cloudflare R2

Generated images need to live somewhere, and Cloudflare R2 is S3-compatible storage with zero egress fees. That last part is the key differentiator — for a site that serves a lot of images, egress costs on S3 or GCS add up fast.

The integration uses the standard AWS S3 SDK pointed at R2's endpoint. If I ever need to migrate to S3 or another S3-compatible service, the code changes are minimal.

NOTE: While I don't use the main Cloudflare service for A Million Pixels, they do donate the service to LQ and honestly the site wouldn't be viable at this point without it.

Alternatives considered: AWS S3, Vercel Blob, Supabase Storage. R2 won on cost (zero egress) and S3 compatibility.

Email

Resend + React Email

Resend handles transactional email — purchase confirmations, moderation notifications, and other emails. The standout feature is that email templates are written as React components using React Email, which means they live in the same codebase and use the same component patterns as the rest of the app.

If you've ever tried to maintain HTML email templates, you know how painful it is. React Email makes it dramatically better.

Alternatives considered: SendGrid, Postmark, AWS SES. Resend's developer experience and the React Email integration put it ahead for a project where I wanted to move fast. I've used most of the others, and Resend has been solid.

Canvas Rendering

PixiJS 8

The heart of the user experience is a 1000×1000 pixel canvas that renders all purchased blocks. That's potentially a million pixels of dynamically loaded, zoomable, pannable content. The DOM can't handle that — you need a WebGL renderer.

PixiJS is the most mature 2D WebGL rendering library. Version 8 brought a new architecture with better performance and a smaller bundle size. It handles the rendering loop, viewport management, and texture loading, while React manages the UI around the canvas. This was mostly decided by the Agents.

Alternatives considered: HTML Canvas 2D API (too slow at this scale), Three.js (overkill for 2D), Konva (not as performant for this use case).

Bot Protection

BotId + Cloudflare Turnstile

Two layers of bot protection that complement each other:

Cloudflare Turnstile is a CAPTCHA alternative that runs as a non-interactive challenge — users don't have to click anything or solve puzzles. It's verified server-side before any purchase is processed.

BotId provides client-side bot detection with browser fingerprinting. It catches automated browsers and headless clients that might bypass Turnstile.

Together they make it significantly harder for bots to abuse the purchase and generation flow without degrading the experience for real users. Have I mentioned lessons learned running LQ?

Alternatives considered: reCAPTCHA (worse UX), hCaptcha, building custom fingerprinting (too much effort for diminishing returns).

Authentication

GitHub OAuth via Supabase Auth

Authentication is only required for one flow: applying for AI credits. The site uses GitHub OAuth through Supabase Auth, which means users sign in with their GitHub account, and Supabase handles the token management, session refresh, and PKCE flow.

GitHub was chosen as the sole OAuth provider because the credit application is aimed at developers. It also allows deduplication — each GitHub account can only submit one application, and it's a reasonable check the submitted is human (for now?).

Alternatives considered: Google OAuth, email magic links, Clerk, Auth0. GitHub OAuth is the simplest option that serves the actual user base and had orthogonal benefits.

Observability

Datadog (APM, RUM, Logs, LLMObs) + OpenTelemetry

Observability is one of those things that's easy to skip in a side project and then regret when something breaks at 2 AM. This project is instrumented more thoroughly than most production apps. I work at Datadog and in a previous role had a terrible on-call life. You can see how I made a lot of these decisions.

  • APM — Distributed tracing across all API routes and external service calls, powered by dd-trace and OpenTelemetry.
  • RUM — Real User Monitoring captures frontend performance, errors, and user sessions in the browser.
  • Logs — Structured logging with automatic PII redaction, shipped to Datadog for correlation with traces.
  • LLMObs — Datadog's LLM Observability tracks every AI call (moderation, generation, embeddings) with input/output, latency, and cost.
  • Monitors and SLOs — Defined as code and synced to Datadog via scripts, so alerting configuration lives in the repo alongside the application code.
  • Feature Flags - Feature flags control which AI model is used for generation and are used for A/B testing UI variants.

OpenTelemetry provides the instrumentation standard, and dd-trace handles the Datadog-specific export. The Next.js instrumentation hook initializes both in a separate webpack bundle from the application code, which is a pattern worth understanding if you're doing Obervability on Vercel in Next.js — module-level singletons aren't shared across bundles... and this took me longer than I'd like to admit to debug.

Alternatives considered: :|

CI/CD

GitHub Actions

The CI pipeline runs on every push and PR, with parallel jobs for linting, type checking, unit tests, integration tests, the production build, and end-to-end tests (Playwright across Chromium and Firefox). Coverage reports and build size reports are posted as PR comments automatically.

There are separate workflows for production releases (with automated rollback on failure), security scanning (npm audit, CodeQL SAST, secrets detection), and dependency updates via Dependabot.

The key philosophy: CI should catch everything that matters, and the pipeline should be fast enough that you never skip it. Parallel job execution keeps the total wall time reasonable.

Alternatives considered: CircleCI, Jenkins... just kidding. Actions may not be super reliable, but they're an easy choice for a project like this.

Blog

MDX via next-mdx-remote

You're reading this on it. Blog posts are MDX files — Markdown with JSX support — processed by next-mdx-remote and rendered as Server Components. Frontmatter is validated with Zod, syntax highlighting uses Shiki via rehype-pretty-code, and posts support custom components like callouts and captioned images.

Posts are statically generated at build time, include Schema.org structured data for SEO, and have an auto-generated RSS feed. It's a simple setup, but it's everything you need for a technical blog without reaching for a CMS. There's even an Obsidian plugin to automate the post scaffolding.

Alternatives considered: None really, I use Markdown and MDX for a lot and didn't see a reason to get fancy here.

Wrapping Up

That's the full stack. It's a lot of pieces, but each one was chosen deliberately and (mostly) for the right reasons. Some of these choices will change over time as the project evolves or as better tools emerge.

If you read the first post, you know this project is about building in public. That extends to the technical decisions too. You can see where I have operational scars. Not everything here is perfect, and I expect to revisit some of these choices. When I do, I'll write about it.

If you have questions about any of these technologies or how they fit together, use the contact form to reach out. And if you haven't already — go claim a block and add your prompt to the canvas.

--jeremy