Postgres (Neon)

A real SQL database, installed from the Vercel Marketplace.

What it is

Vercel doesn't run databases itself any more: the Marketplace installs them from providers, bills through Vercel and sets the connection variables on your project. Neon is serverless Postgres: storage and compute are separate, compute scales to zero when idle, and the @neondatabase/serverless driver sends queries over HTTPS, so short-lived functions don't need a connection pool. This database lives in iad1, the same region as the lab's functions.

When to use it

  • Relational data: users, orders, anything with joins or transactions
  • Data that must be consistent and durable
  • When you want plain SQL, or an ORM like Drizzle or Prisma on top
Official docs ↗

1. Write: sign the guestbook (Server Action → INSERT)

Checking your session…

Notes are limited to 280 characters (by the app and again by the table's CHECK constraints), rate-limited with Redis, and only the newest 50 are kept.

2. Read: the latest notes (SELECT)

Querying Postgres…

Show code · src/lib/db.ts
import { neon } from "@neondatabase/serverless";
import { KEEP_NOTES } from "@/lib/notes";

// Neon Postgres, through the Vercel Marketplace. The serverless driver sends
// each query over HTTPS: no connection pool to manage, which suits functions
// that start and stop. The table is created by db/schema.sql (npm run db:migrate).

export const dbConfigured = () => Boolean(process.env.DATABASE_URL);
export const sql = () => neon(process.env.DATABASE_URL!);

export type Note = { id: string; author: string; body: string; created_at: string };

// Values are passed as template parameters, never pasted into the SQL text,
// so they can't change the query (no SQL injection).
export async function latestNotes(limit = 20) {
  return (await sql()`SELECT id, author, body, created_at FROM lab_notes ORDER BY created_at DESC LIMIT ${limit}`) as Note[];
}

export async function addNote(author: string, body: string) {
  const db = sql();
  await db`INSERT INTO lab_notes (author, body) VALUES (${author}, ${body})`;
  // Keep the table small: only the newest notes survive.
  await db`DELETE FROM lab_notes WHERE id NOT IN (SELECT id FROM lab_notes ORDER BY created_at DESC LIMIT ${KEEP_NOTES})`;
}
Show code · src/app/postgres/actions.ts
"use server";

import { refresh } from "next/cache";
import { verifyHuman } from "@/lib/botid";
import { currentSession } from "@/lib/current-session";
import { addNote, dbConfigured } from "@/lib/db";
import { checkNote } from "@/lib/notes";
import { checkLimit, retryIn, writeLimit } from "@/lib/redis";

export type NoteState = { message: string; error: boolean } | null;

export async function postNote(_prev: NoteState, formData: FormData): Promise<NoteState> {
  if (!dbConfigured()) return { message: "Postgres isn't connected for this environment.", error: true };
  const session = await currentSession();
  if (!session) return { message: "Sign in first (on the Proxy page): notes are signed with your lab name.", error: true };

  const note = checkNote(formData.get("body"));
  if (!note.ok) return { message: note.error, error: true };

  // BotID (Basic, free): the form's page solved an invisible challenge; scripts posting directly haven't.
  if ((await verifyHuman()).isBot) return { message: "Bot detected: notes can only be posted from the page.", error: true };

  const limit = await checkLimit(writeLimit);
  if (limit && !limit.success) return { message: `Slow down: try again in ${retryIn(limit.reset)} s.`, error: true };

  await addNote(session.name, note.body);
  refresh();
  return { message: "Saved.", error: false };
}