Skip to content

Repository files navigation

A URL shortener, and a place to try out App Router / RSC Next.js with Kysely, Turso and Clerk.

Deployed at l.kalski.xyz.

Getting started

Toolchain versions are pinned in mise.toml (mise):

mise install                # Node + pnpm
cp .env.example .env.local  # then fill it in, see below
pnpm install
pnpm dev

You need a Clerk application for auth and a Turso database.

Environment

Variable Notes
DATABASE_URL libsql://…turso.io?authToken=…
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY public; the app cannot render a page without it
CLERK_SECRET_KEY can mint a session for any user — keep it out of CI
NEXT_PUBLIC_CLERK_SIGN_{IN,UP}_URL /sign-in, /sign-up
NEXT_PUBLIC_CLERK_SIGN_{IN,UP}_FALLBACK_REDIRECT_URL / — note the name: AFTER_SIGN_IN_URL etc. are silently ignored

Architecture

Every route runs on the edge runtime. That is the constraint behind most of the decisions here: on edge, @libsql/client resolves to its web build, which speaks HTTP to a libsql server and cannot open a local SQLite file. So there is no "just use a file locally" option — the e2e suite runs a libsql container instead.

Auth is Clerk middleware, in proxy.ts:

Path Behaviour
/ protectedauth.protect() redirects to sign-in
/api/create middleware runs; the handler checks auth() itself and answers 401
/sign-in, /sign-up middleware runs so Clerk's handshake can complete
/[slug] public — a route handler, outside the matcher, never touches Clerk
/api/health public — liveness, no Clerk

clerkMiddleware() protects nothing on its own — the protected set above is what does it. Removing that call makes / public with no error anywhere.

/[slug] caches lookups with unstable_cache, but never trusts a cached miss — a link created after someone first tried the slug would otherwise keep 404ing for an hour.

Database

Schema changes are Kysely migrations in db_migration/migrations. They run in order and are recorded in a kysely_migration table, so re-running applies only what is pending.

DATABASE_URL=<url> pnpm db:status    # read-only: applied vs pending
DATABASE_URL=<url> pnpm db:migrate   # apply everything pending

pnpm kysely-generate migrates a local local.db and regenerates types/db.d.ts from it. Run it after adding a migration so the generated types match.

⚠️ Migrations are never applied automatically

Nothing runs them for you — not the Vercel build, not CI, not the app at startup. A schema change reaches production only when a human runs pnpm db:migrate against it.

So a deploy can ship code expecting a column that does not exist. Order matters:

Change Order
Additive (new table/column/index) migrate first, then deploy
Destructive (drop/rename a column) deploy code that stopped using it first, then migrate

Run db:status before db:migrate — it connects and reports without changing anything, which also confirms you are pointed at the database you think you are.

Checks

pnpm lint          # oxlint
pnpm format        # oxfmt, write
pnpm format:check  # oxfmt, check only
pnpm test:unit     # node --test
pnpm test:e2e      # Playwright — needs Docker running

CI runs lint, format:check, test:unit, test:e2e and the build on every push and pull request.

Deployment

Vercel, on push. next build only builds — it does not migrate.

⚠️ ENABLE_EXPERIMENTAL_COREPACK=1 is required

Set on Production and Preview. Do not remove it.

Vercel supports pnpm 6–10 natively and infers the version from lockfileVersion, which pnpm 11 leaves at 9.0. Without corepack, Vercel picks pnpm 9, which rejects a pnpm-workspace.yaml that has no packages: field — and this one intentionally has only allowBuilds and overrides. The build fails with ERROR packages field missing or empty. With the variable set, Vercel reads packageManager from package.json instead.

Gotchas

  • @types/node is pinned to the 24.x line to match the Node version in mise.toml. Tooling keeps offering newer majors; they describe APIs that do not exist in Node 24, so code would type-check and then fail at runtime. Bump it only alongside the runtime.
  • pnpm-workspace.yaml carries real configurationallowBuilds (pnpm 11 blocks dependency build scripts by default) and an overrides entry deduping postcss, which Next pins to a version carrying advisories.

About

URL-shortener using Next.js, RSC & kysely.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages