NiteCal: Fullstack Astro 101 — Wrangler, D1, Drizzle
You can go from idea to a working MVP in under an hour, on a domain you own, for less than a cup of coffee. No AI. Just the excellent Cloudflare + Astro stack.
What are we building?
Had enough of people trying to book meetings with you during the day? - NiteCal.lol is a calendar app for people who are only available for meetings at night.
I’ve built on this stack many times already — Claude usually wires it up. Well, tbh, I hit my Claude-limit and had to figure it out myself. So I stripped the framework out and built with the actual pieces. That’s when it clicked. Wrangler owns the runtime. D1 is just a SQLite file on disk during dev. Drizzle is a TypeScript ORM layer, - it generates your SQL and tracks migrations.
The Stack
- Wrangler 4 — Cloudflare’s local dev runtime. Runs your worker (web server), simulates D1 through Sqlite.
- Cloudflare D1 — Serverless SQLite at the edge. Free tier is generous.
- Drizzle ORM v1 RC — TypeScript ORM. Schema-as-code, migration files, and Drizzle Studio for browsing data in the browser.
Setup
npm init -y
Dependencies:
{
"dependencies": {
"drizzle-orm": "^1.0.0-rc.2",
"drizzle-kit": "^1.0.0-rc.2"
},
"devDependencies": {
"wrangler": "^4.90.0",
"node-addon-api": "^8.7.0",
"node-gyp": "^12.3.0"
}
}
Install wrangler first and you’ll likely hit an error about sharp needing native bindings. Add node-addon-api and node-gyp as dev dependencies and reinstall. Not optional, not obvious.
Scripts:
"scripts": {
"db:generate": "drizzle-kit generate --config drizzle.config.ts",
"db:migrate": "drizzle-kit migrate --config drizzle.config.ts",
"db:studio": "drizzle-kit studio --config drizzle.config.ts"
}
Wrangler Config
wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "nitecal",
"compatibility_date": "2025-04-01",
"main": "./src/worker.ts",
"d1_databases": [
{
"binding": "DB",
"database_name": "nitecal-db",
"database_id": "local-only",
"migrations_dir": "./db/migrations"
}
]
}
How Drizzle Thinks About Migrations
If you’ve used Rails, the mental model is backwards. Rails: you write a migration, it changes the database. Drizzle: you define your schema in TypeScript, then Drizzle diffs it against what’s in the database and generates the migration for you. The schema drives everything. With Drizzle - you need two commands: generate and migrate.
The Schema
src/models/event.ts:
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const events = sqliteTable('events', {
at: text('timestamp').notNull()
});
SQLite has no date type. The supported types are integer, real, text, blob. So at is stored as text. Drizzle lets you layer behaviour on top and treat it as a timestamp. For now, keeping it minimal. More fields come in video 102.
drizzle.config.ts:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/models/*.ts',
out: './db/migrations',
dialect: 'sqlite',
dbCredentials: {
url: './nitecal.db',
},
});
Note ./nitecal.db — that file doesn’t exist yet. We’ll get to it.
The Bit Everyone Gets Confused By
Run wrangler dev to start the local dev server. This does something important in the background: it creates a real SQLite file to simulate D1.
.wrangler/state/v3/d1/miniflare-D1DatabaseObject/e1d57d4c...sqlite
That path is deeply nested and the filename is a content hash.
This is where the symlink comes in:
ln -s .wrangler/state/v3/d1/miniflare-D1DatabaseObject/<hash>.sqlite nitecal.db
Now ./nitecal.db is a pointer to whatever wrangler created. Drizzle’s config points at ./nitecal.db — it doesn’t need to know the real path.
Wrangler and Drizzle are now looking at the exact same file. When wrangler dev is running and you open Drizzle Studio, you’re browsing the live dev database. Not a copy. Same file.
Run the Migration
npm run db:generate # creates db/migrations/20260508_*/migration.sql
npm run db:migrate # applies it to the SQLite via the symlink
Drizzle v1 generates migrations as nested folders (20260508135710_name/migration.sql). This is new — it’s what makes commutative migrations work in teams. It also means the old wrangler d1 migrations apply command no longer works with Drizzle v1, since wrangler expects flat .sql files. drizzle-kit migrate handles it correctly.
Open Studio
npm run db:studio
A browser tab opens. Your events table is there. You can create, edit, and delete rows directly in the UI.
Studio lets you edit schema too, but don’t. If you change a column in Studio it bypasses Drizzle entirely - no migration file, no version control, just a raw schema change that will disappear or conflict the next time you run db:migrate. Manage data in Studio, manage schema in code. If you just joined your first dev team - changing schema like this will make you very unpopular.
That’s the MVP. We are leaning pretty hard on Studio, but its calendar component is great.
Business email from Spacemail
Nobody is going to take your project seriously with a Gmail address. A “business” email for less than $1 per month? Why not.
Get [email protected] from Spaceship Business Email
Spacemail has native apps on Android and iOS — I’ve been using them for over a month with zero issues. All my test emails land in the inbox, not spam.
What’s Next
Next up: Lets install Astro and build some pages.
Subscribe to Skills Weekly, a digest of the hottest plugins & industry news. Only one email a week.
We respect your privacy. Unsubscribe anytime. Privacy Policy
Questions or feedback? Reach out on X @GetSkillsdev
← Back