All code

Repository

Margin

Turns a folder of photographs into a written chapter about the day they were taken, with artwork made for it.

A scrapbook that remembers more than you did.


Private repository. Everything below is written from the code; none of it is a link you can follow.

What it is

You upload a batch of photos and it builds a chapter. Every photo is enriched with what can be inferred — time, place, weather, faces, scene emotion, subjects — and the chapter is written in one of four voices, given a visual identity generated for it, and rendered as an editorial scroll, a paged book, or a scrapbook. Chapters accumulate into a library with people memory, maps and year portraits.

Why it exists

Everyone has ten thousand photos and no story. The products that address that either generate slop — Your Week in Review — or demand hours of manual scrapbooking. The bet is that enrichment plus generation closes the gap: if the system knows it was raining, that these three people recur, and that this was the last photo of a trip, it can write something worth keeping.

As a business

Consumer subscription is the obvious shape, and the bindable books point at print as an upsell. I did not pursue it — there is no billing, no pricing, no paywall. The economics are the hard part and I know it: a chapter costs a photo-enrichment pass plus a writing pass plus image generation, which is closer to a print product's margins than a software product's.

Stack

Client
Next.js 14 · TypeScript · Tailwind
Backend
Supabase · Postgres · Auth · Storage
AI
Claude · OpenAI · Nano Banana 2 · Recraft

The decision

The problem

A template makes every chapter look the same, which undercuts the whole premise that each chapter is a distinct memory. But freely-generated design is usually worse than a good template — unconstrained generation produces the clip-art look that makes a product feel cheap.

What I did

The theme is a structured output — palette, fonts, frames, motifs, layout — generated per chapter and then rendered by the app's own components. The model chooses within a system rather than drawing the page, and rendering is constrained to three formats, so novelty lives in the palette-and-motif layer while typography and layout stay under the app's control.

What it costs

A narrower design space than free generation, and a real risk that four voices times three formats still converges on a house style. It also multiplies what has to look right: every theme the model can emit must render acceptably in all three layouts, and there is no way to test that exhaustively — which is why the repository is full of kept output directories from generation runs.

Where it ended

Parked rather than abandoned. Five hundred and nineteen of its seven hundred and seventy-one commits land in a single month, then it tapers. It is deployed and reachable, and the last substantial push was an editorial redesign of how it reads rather than what it does.

A piece of the code

margin-web/lib/theme/types.ts

/** A ThemeKit  high-level vibe. Built-in kits live in design_space.ts *  as const data; AI-generated kits live in the `theme_kits` table and *  are loaded at theme-generation time. Same shape. */export type ThemeKit = {  short_id: string  label: string  /** One short sentence the LLM uses to decide whether the kit fits   *  the chapter. Keep concrete: places, occasions, moods. */  use_when: string  palette: PaletteFamily  font_pair: FontPairId  motif_set: MotifSetId  frame: FrameKindId  texture: TextureId  density: DensityId  /** "builtin" for ones we ship with the app; "ai" for ones invented   *  during generation; "user" for ones the user hand-authored. */  source: "builtin" | "ai" | "user"}
The model does not draw the page, it fills in this shape. Every field is an identifier into a system the app already knows how to render, so an invented theme and a hand-authored one are the same object and go down the same path. `source` is the only thing that distinguishes them, and it exists for auditing rather than for rendering.

Excerpt — trimmed for reading, not a full file.

Ask about this project