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
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 };
}