# Deploy a Next.js blog with PostgreSQL on Yeeted

A Yeeted sample: Next.js App Router on managed postgres. What it
demonstrates:

- **The `next.js` build branch**: a `package-lock.json` picks `npm ci`, the
  synthesized image runs `next build`, and `next start` serves on `$PORT`.
- **Managed postgres with self-migration**: `DATABASE_URL` is injected by the
  platform; the app applies `migrations/*.sql` in order on first use, tracked
  in `schema_migrations` (tracked across sequential restarts).
- **Graceful degradation**: no `DATABASE_URL` runs an in-memory store, which
  is what the unit tests use and what `next build` prerenders against.

## The two things that catch people

**`next build` executes your server components.** It prerenders every route it
can, at build time, on a machine with no database. A page that queries postgres
without opting out is either a build failure or, worse, a static file baked
from an empty store and served forever, so the app deploys clean and shows
nothing. `app/page.js` and the API route set `export const dynamic =
"force-dynamic"`; `/stats` deliberately does not, and revalidates instead.

**`pg` must stay out of the bundle.** `serverExternalPackages: ["pg"]` in
`next.config.mjs`. Without it `next build` tries to resolve pg's optional
native bindings at compile time and fails.

## Routes

| Route | What it is |
|---|---|
| `/` | server-rendered per request, lists posts from postgres |
| `/stats` | statically generated, `revalidate = 10` |
| `/api/posts` | `GET` lists, `POST` creates (`{"title","body"}`) |
| `/healthz` | `ok` |

`/stats` is the one route that makes `next start` write to `.next/cache` at
runtime, on a root filesystem the platform mounts read-only except for `/tmp`.
It is here so that the case is exercised rather than avoided.

Tests: `npm test` (unit, zero services) · `./smoke.sh https://...` (smoke).

## Run locally

Use a Node.js version supported by the pinned Next.js dependency (Node 20.9+
for Next.js 16). From `node/blog-next` inside the examples tree:

```sh
npm ci
npm test
npm run dev
```

Open `http://localhost:3000`. Without `DATABASE_URL`, posts live in memory and
are lost on restart. For a database-backed local run, provide a PostgreSQL
connection string through your environment before starting the server. The
unit tests exercise the memory store, not PostgreSQL or rendering.

## Deploy and verify

Follow the [deployment walkthrough](getting-started.md), using this
folder as the source root. Include the lockfile, `next.config.mjs`, migrations,
and `yeeted.yaml`. The manifest requests PostgreSQL. The store opens lazily,
applies its migration on first use, and exposes its kind in API responses.

```sh
sh smoke.sh "$PREVIEW_URL"
```

Expect health and page checks to pass, a new post to return 201, and
`GET /api/posts` to contain the new post and `"store":"postgres"`. The
script also fetches `/stats` and rejects a blank title with 400. Merely
rendering `/stats` does not verify that its cached count eventually changes;
check again after its revalidation interval when testing that behavior.

For a manual write:

```sh
curl -i "$PREVIEW_URL/api/posts" -H 'Content-Type: application/json'   -d '{"title":"My first Yeeted post","body":"Deployed from my coding agent."}'
curl -fsS "$PREVIEW_URL/api/posts"
```

## What to change before real use

There is no author authentication. Migrations are recorded, but the sample
migration runner does not serialize concurrent instances. Use a coordinated
migration step before running multiple replicas. A rollback of application
code does not undo a schema migration. Verify production rendering and
PostgreSQL behavior separately from the in-memory unit tests.

[All examples](index.md) · [Deploy an example](getting-started.md)
