···11+# WebAuthn/Passkey Configuration
22+# In development, these default to localhost values
33+# Only needed when deploying to production
44+55+# Relying Party ID - your domain name
66+# Must match the domain where your app is hosted
77+# RP_ID=tacy-stack.app
88+99+# Origin - full URL of your app
1010+# Must match exactly where users access your app
1111+ORIGIN=http://localhost:3000
1212+1313+# Environment (set to 'production' in production)
1414+NODE_ENV=development
+26
.gitignore
···11+# Bun
22+node_modules/
33+bun.lockb
44+55+# Database
66+*.db
77+*.db-shm
88+*.db-wal
99+drizzle/
1010+1111+# Environment
1212+.env
1313+.env.local
1414+1515+# IDE
1616+.vscode/
1717+.idea/
1818+*.swp
1919+*.swo
2020+2121+# OS
2222+.DS_Store
2323+Thumbs.db
2424+2525+# Logs
2626+*.log
+550
AGENTS.md
···11+# Tacy Stack - Agent Guidelines
22+33+This is a minimal full-stack web application starter built on the Bun fullstack pattern. It demonstrates passkey authentication, user-specific data storage, and reactive web components.
44+55+## Project Overview
66+77+**What is Tacy Stack?**
88+A TypeScript-based web stack using:
99+- **Bun** - Fast JavaScript runtime and bundler
1010+- **TypeScript** - Strict typing with decorators enabled
1111+- **Lit** - Lightweight (~8-10KB) web components library
1212+- **Drizzle ORM** - Type-safe SQLite database access
1313+- **Passkeys (WebAuthn)** - Passwordless authentication
1414+1515+**Demo Application Features:**
1616+- User registration and login via passkeys (no passwords!)
1717+- User-specific click counter stored in database
1818+- Reactive UI with Lit web components
1919+- Session management with cookies
2020+2121+## Commands
2222+2323+```bash
2424+# Install dependencies
2525+bun install
2626+2727+# Development server with hot reload (default: localhost:3000)
2828+bun dev
2929+3030+# Database management
3131+bun run db:generate # Generate migration files from schema changes
3232+bun run db:push # Push schema directly to database (for development)
3333+bun run db:studio # Open Drizzle Studio (visual database browser)
3434+3535+# Testing
3636+bun test # Run all tests
3737+```
3838+3939+**IMPORTANT:** Never run `bun dev` yourself - the user always has it running already with HMR enabled.
4040+4141+## Tech Stack Philosophy
4242+4343+### NO FRAMEWORKS
4444+4545+**Explicitly forbidden:**
4646+- React, React DOM
4747+- Vue
4848+- Svelte
4949+- Angular
5050+- Any framework with virtual DOM or large runtime
5151+5252+**Why?** This project prioritizes:
5353+- **Speed:** Minimal JavaScript, fast load times
5454+- **Small bundles:** Keep total JS under 50KB
5555+- **Native web platform:** Web standards over framework abstractions
5656+- **Simplicity:** Vanilla HTML, CSS, and TypeScript
5757+5858+**Allowed:**
5959+- Lit (~8-10KB) for reactive web components
6060+- Native Web Components API
6161+- Plain JavaScript/TypeScript
6262+- Native DOM APIs
6363+6464+### When to use Lit
6565+6666+**Use Lit for:**
6767+- Components with reactive properties (auto-updates on data changes)
6868+- Complex components needing scoped styles
6969+- Form controls with internal state
7070+- Components with lifecycle needs
7171+7272+**Skip Lit for:**
7373+- Static content (use plain HTML)
7474+- Simple one-off interactions (use vanilla JS)
7575+- Anything without reactive state
7676+7777+## Project Structure
7878+7979+Based on Bun's fullstack pattern where HTML files are imported as modules:
8080+8181+```
8282+src/
8383+ index.ts # Server entry point, imports HTML routes
8484+ db/
8585+ db.ts # Drizzle database instance
8686+ schema.ts # Database schema (Drizzle tables)
8787+ lib/
8888+ auth.ts # User/session management
8989+ passkey.ts # WebAuthn passkey logic
9090+ counter.ts # Counter business logic
9191+ middleware.ts # Auth middleware
9292+ client-passkey.ts # Client-side passkey helpers
9393+ pages/
9494+ index.html # Route entry point (imports components)
9595+ components/
9696+ auth.ts # Login/register Lit component
9797+ counter.ts # Counter Lit component
9898+ styles/
9999+ main.css # Global styles
100100+public/ # Static assets (if needed)
101101+```
102102+103103+**File flow:**
104104+1. `src/index.ts` imports HTML: `import indexHTML from "./pages/index.html"`
105105+2. HTML imports components: `<script type="module" src="../components/auth.ts"></script>`
106106+3. HTML links styles: `<link rel="stylesheet" href="../styles/main.css">`
107107+4. Components self-register via `@customElement` decorator
108108+5. Bun bundles everything automatically during development
109109+110110+## Database (Drizzle ORM)
111111+112112+**Database file:** `tacy-stack.db` (SQLite, auto-created)
113113+114114+**Schema location:** `src/db/schema.ts`
115115+116116+**Tables:**
117117+- `users` - User accounts (id, email, name, avatar, created_at)
118118+- `sessions` - Active sessions (id, user_id, ip, user_agent, expires_at)
119119+- `passkeys` - WebAuthn credentials (id, user_id, credential_id, public_key, counter, etc.)
120120+- `counters` - User click counters (user_id, count, updated_at)
121121+122122+**Making schema changes:**
123123+1. Edit `src/db/schema.ts` (modify tables using Drizzle syntax)
124124+2. Run `bun run db:push` to apply changes to database
125125+3. For production, use `bun run db:generate` to create migrations
126126+4. Schema changes are type-safe and auto-complete in your IDE
127127+128128+**Querying patterns:**
129129+```typescript
130130+import { eq } from "drizzle-orm";
131131+import db from "../db/db";
132132+import { users } from "../db/schema";
133133+134134+// Select with where clause
135135+const user = db.select().from(users).where(eq(users.id, userId)).get();
136136+137137+// Insert
138138+db.insert(users).values({ email, name, avatar }).run();
139139+140140+// Update
141141+db.update(users).set({ name: "New Name" }).where(eq(users.id, userId)).run();
142142+143143+// Delete
144144+db.delete(users).where(eq(users.id, userId)).run();
145145+```
146146+147147+**Important:** Use `.get()` for single results, `.all()` for arrays, `.run()` for mutations.
148148+149149+## TypeScript Configuration
150150+151151+**Strict mode enabled:**
152152+- `strict: true`
153153+- `noFallthroughCasesInSwitch: true`
154154+- `noUncheckedIndexedAccess: true`
155155+- `noImplicitOverride: true`
156156+157157+**Decorators:**
158158+- `experimentalDecorators: true` (required for Lit's `@customElement`, `@property`, etc.)
159159+- `useDefineForClassFields: false` (required for Lit decorators)
160160+161161+**Module system:**
162162+- `moduleResolution: "bundler"`
163163+- `module: "Preserve"`
164164+- Can import `.ts` extensions directly
165165+- JSX: `preserve` (NOT react-jsx - we don't use React!)
166166+167167+**Deliberately disabled:**
168168+- `noUnusedLocals: false`
169169+- `noUnusedParameters: false`
170170+- `noPropertyAccessFromIndexSignature: false`
171171+172172+## Bun Usage
173173+174174+**Always use Bun instead of Node.js:**
175175+- ✅ `bun <file>` NOT `node <file>` or `ts-node <file>`
176176+- ✅ `bun test` NOT `jest` or `vitest`
177177+- ✅ `bun build <file>` NOT `webpack` or `esbuild`
178178+- ✅ `bun install` NOT `npm install`
179179+- ✅ `bun run <script>` NOT `npm run <script>`
180180+181181+**Bun built-in APIs (prefer over npm packages):**
182182+- `Bun.serve()` for HTTP server (don't use Express)
183183+- `bun:sqlite` for SQLite (but we use Drizzle which wraps it)
184184+- `Bun.file()` for file I/O (prefer over `node:fs`)
185185+- `.env` auto-loads (no dotenv package needed)
186186+187187+## Server Setup (Bun.serve)
188188+189189+Use `Bun.serve()` with the `routes` pattern:
190190+191191+```typescript
192192+import indexHTML from "./pages/index.html";
193193+194194+Bun.serve({
195195+ port: 3000,
196196+ routes: {
197197+ "/": indexHTML, // HTML route
198198+ "/api/users/:id": { // API route with params
199199+ GET: (req) => {
200200+ return new Response(JSON.stringify({ id: req.params.id }));
201201+ },
202202+ },
203203+ },
204204+ development: {
205205+ hmr: true, // Hot module reloading
206206+ console: true, // Enhanced console output
207207+ },
208208+});
209209+```
210210+211211+**Route params:** Access via `req.params.paramName`
212212+213213+**Request helpers:**
214214+- `await req.json()` - Parse JSON body
215215+- `req.headers.get("cookie")` - Get header
216216+- `req.url` - Full URL
217217+218218+## Frontend Pattern
219219+220220+**HTML files import TypeScript modules directly:**
221221+222222+```html
223223+<!DOCTYPE html>
224224+<html lang="en">
225225+<head>
226226+ <meta charset="UTF-8">
227227+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
228228+ <title>Page Title - Tacy Stack</title>
229229+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>🥞</text></svg>">
230230+ <link rel="stylesheet" href="../styles/main.css">
231231+</head>
232232+<body>
233233+ <header>
234234+ <nav>
235235+ <h1>🥞 Tacy Stack</h1>
236236+ <auth-component></auth-component>
237237+ </nav>
238238+ </header>
239239+240240+ <main>
241241+ <h1>Page Title</h1>
242242+ <counter-component count="0"></counter-component>
243243+ </main>
244244+245245+ <script type="module" src="../components/auth.ts"></script>
246246+ <script type="module" src="../components/counter.ts"></script>
247247+</body>
248248+</html>
249249+```
250250+251251+**Standard HTML template parts:**
252252+- Pancake emoji favicon
253253+- Proper meta tags (charset, viewport)
254254+- Auth component in header nav
255255+- Main content area
256256+- Module scripts at end of body
257257+258258+**No build step needed:** Bun transpiles and bundles automatically during development.
259259+260260+## Lit Web Components
261261+262262+**Basic component structure:**
263263+264264+```typescript
265265+import { LitElement, html, css } from "lit";
266266+import { customElement, property, state } from "lit/decorators.js";
267267+268268+@customElement("my-component")
269269+export class MyComponent extends LitElement {
270270+ // Public reactive properties (can be set via HTML attributes)
271271+ @property({ type: String }) name = "World";
272272+273273+ // Private reactive state (internal only)
274274+ @state() private count = 0;
275275+276276+ // Scoped styles using css tagged template
277277+ static override styles = css`
278278+ :host {
279279+ display: block;
280280+ padding: 1rem;
281281+ }
282282+ .greeting {
283283+ color: var(--primary);
284284+ }
285285+ `;
286286+287287+ // Render using html tagged template
288288+ override render() {
289289+ return html`
290290+ <div class="greeting">
291291+ Hello, ${this.name}!
292292+ <button @click=${() => this.count++}>
293293+ Clicked ${this.count} times
294294+ </button>
295295+ </div>
296296+ `;
297297+ }
298298+}
299299+```
300300+301301+**Key Lit features:**
302302+- `@customElement("tag-name")` - Register component
303303+- `@property()` - Public reactive property (triggers re-render)
304304+- `@state()` - Private reactive state (triggers re-render)
305305+- `html` - Template literal for rendering
306306+- `css` - Scoped styles (don't leak out)
307307+- Event handlers: `@click=${handler}` or `@click=${() => ...}`
308308+- Automatic re-rendering when properties/state change
309309+310310+## Design System
311311+312312+**Color palette (CSS variables):**
313313+```css
314314+:root {
315315+ /* Named colors */
316316+ --yale-blue: #033f63ff; /* dark blue */
317317+ --stormy-teal: #28666eff; /* medium teal */
318318+ --muted-teal: #7c9885ff; /* soft teal */
319319+ --dry-sage: #b5b682ff; /* sage green */
320320+ --soft-peach: #fedc97ff; /* warm peach */
321321+322322+ /* Semantic assignments */
323323+ --text: var(--yale-blue);
324324+ --background: var(--soft-peach);
325325+ --primary: var(--stormy-teal);
326326+ --secondary: var(--muted-teal);
327327+ --accent: var(--dry-sage);
328328+}
329329+```
330330+331331+**CRITICAL COLOR RULES:**
332332+- ❌ NEVER hardcode colors: `#4f46e5`, `white`, `red`, etc.
333333+- ✅ ALWAYS use CSS variables: `var(--primary)`, `var(--accent)`, `var(--text)`, etc.
334334+335335+**Dimension rules:**
336336+- Use `rem` for all sizes (not `px`)
337337+- Base: 16px = 1rem
338338+- Common values: `0.5rem`, `1rem`, `1.5rem`, `2rem`, `3rem`
339339+- Max widths: `48rem` (content), `56rem` (forms/data)
340340+- Spacing scale: `0.25rem`, `0.5rem`, `0.75rem`, `1rem`, `1.5rem`, `2rem`, `3rem`
341341+342342+## Authentication & Sessions
343343+344344+**Flow:**
345345+1. User enters email/name and clicks "Register"
346346+2. `POST /api/auth/register` creates user and session
347347+3. `GET /api/auth/passkey/register/options` gets WebAuthn options
348348+4. Client calls `startRegistration()` from `@simplewebauthn/browser`
349349+5. `POST /api/auth/passkey/register/verify` stores passkey
350350+6. Session cookie set, user logged in
351351+352352+**Login flow:**
353353+1. User clicks "Sign In"
354354+2. `GET /api/auth/passkey/authenticate/options` gets WebAuthn challenge
355355+3. Client calls `startAuthentication()` from `@simplewebauthn/browser`
356356+4. `POST /api/auth/passkey/authenticate/verify` verifies and creates session
357357+5. Session cookie set, user logged in
358358+359359+**Session management:**
360360+- Sessions stored in `sessions` table
361361+- Cookie name: `session`
362362+- Duration: 7 days
363363+- HTTPOnly, SameSite=Strict
364364+365365+**Auth helpers:**
366366+- `getSessionFromRequest(req)` - Extract session ID from cookie
367367+- `getUserBySession(sessionId)` - Get user from session
368368+- `requireAuth(req)` - Middleware that throws if not authenticated
369369+370370+## API Response Patterns
371371+372372+**Success:**
373373+```typescript
374374+return new Response(JSON.stringify({ count: 42 }), {
375375+ headers: { "Content-Type": "application/json" },
376376+});
377377+```
378378+379379+**Error:**
380380+```typescript
381381+return new Response(JSON.stringify({ error: "Not authenticated" }), {
382382+ status: 401,
383383+});
384384+```
385385+386386+**With cookie:**
387387+```typescript
388388+return new Response(JSON.stringify(user), {
389389+ headers: {
390390+ "Content-Type": "application/json",
391391+ "Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=604800`,
392392+ },
393393+});
394394+```
395395+396396+## Code Conventions
397397+398398+**Naming:**
399399+- PascalCase: Components, classes (`AuthComponent`, `CounterComponent`)
400400+- camelCase: Functions, variables (`getUserByEmail`, `sessionId`)
401401+- kebab-case: File names, custom element tags (`auth.ts`, `counter-component`)
402402+403403+**File organization:**
404404+- Place tests next to code: `foo.ts` → `foo.test.ts`
405405+- Keep related code together (e.g., all auth logic in `lib/auth.ts`)
406406+- Components are self-contained (logic + styles + template)
407407+408408+**Before writing code:**
409409+1. Check if library exists (look at imports, package.json)
410410+2. Read similar code for patterns
411411+3. Match existing style
412412+4. Never assume libraries are available - verify first
413413+414414+## Testing
415415+416416+Use `bun test` with Bun's built-in test runner:
417417+418418+```typescript
419419+import { test, expect } from "bun:test";
420420+421421+test("creates user with avatar", async () => {
422422+ const user = await createUser("test@example.com", "Test User");
423423+ expect(user.email).toBe("test@example.com");
424424+ expect(user.avatar).toBeTruthy();
425425+});
426426+```
427427+428428+**What to test:**
429429+- ✅ Security-critical functions (auth, sessions, passkeys)
430430+- ✅ Complex business logic (counter operations)
431431+- ✅ Edge cases (empty inputs, missing data)
432432+- ❌ Simple getters/setters
433433+- ❌ Framework/library code
434434+- ❌ One-line utilities
435435+436436+**Test file naming:** `*.test.ts` (auto-discovered by Bun)
437437+438438+## Environment Variables
439439+440440+Copy `.env.example` to `.env`:
441441+442442+```bash
443443+# WebAuthn/Passkey Configuration
444444+RP_ID=localhost # Your domain (localhost for dev)
445445+ORIGIN=http://localhost:3000 # Full app URL
446446+447447+# Environment
448448+NODE_ENV=development
449449+```
450450+451451+**Production values:**
452452+- `RP_ID` - Your domain (e.g., `tacy-stack.app`)
453453+- `ORIGIN` - Full public URL (e.g., `https://tacy-stack.app`)
454454+455455+**Important:** Bun auto-loads `.env`, no dotenv package needed.
456456+457457+## Common Tasks
458458+459459+### Adding a new route
460460+1. Create HTML file in `src/pages/` (e.g., `about.html`)
461461+2. Import in `src/index.ts`: `import aboutHTML from "./pages/about.html"`
462462+3. Add to routes: `"/about": aboutHTML`
463463+464464+### Adding a new API endpoint
465465+Add to `routes` object in `src/index.ts`:
466466+```typescript
467467+"/api/my-endpoint": {
468468+ GET: async (req) => {
469469+ const userId = await requireAuth(req);
470470+ // ... your logic
471471+ return new Response(JSON.stringify({ data }));
472472+ },
473473+},
474474+```
475475+476476+### Adding a new component
477477+1. Create `src/components/my-component.ts`
478478+2. Use `@customElement("my-component")` decorator
479479+3. Import in HTML: `<script type="module" src="../components/my-component.ts"></script>`
480480+4. Use in HTML: `<my-component></my-component>`
481481+482482+### Adding a database table
483483+1. Edit `src/db/schema.ts`:
484484+```typescript
485485+export const myTable = sqliteTable("my_table", {
486486+ id: integer("id").primaryKey({ autoIncrement: true }),
487487+ name: text("name").notNull(),
488488+});
489489+```
490490+2. Run `bun run db:push` (pushes schema to database)
491491+3. Query: `db.select().from(myTable).all()`
492492+493493+### Adding styles
494494+- Global styles: Edit `src/styles/main.css`
495495+- Component-scoped: Use `static styles = css\`...\`` in Lit component
496496+- Always use CSS variables for colors
497497+498498+## Formatting & Linting
499499+500500+**Biome** is configured for formatting and linting:
501501+- Indent style: Tabs
502502+- Quote style: Double quotes
503503+- Auto-organize imports enabled
504504+505505+**LSP support:** Biome provides IDE integration for formatting/linting.
506506+507507+## Gotchas
508508+509509+1. **Don't use Node.js commands** - Use `bun` not `node`, `npm`, `npx`
510510+2. **Don't install Express/Vite** - Bun has built-in equivalents
511511+3. **NEVER use React** - Explicitly forbidden, use Lit or vanilla JS
512512+4. **Import .ts extensions** - Bun allows direct `.ts` imports
513513+5. **No dotenv needed** - Bun loads `.env` automatically
514514+6. **HTML imports are special** - They trigger Bun's bundler
515515+7. **Bundle size matters** - Measure impact before adding libraries
516516+8. **Drizzle queries** - Use `.get()` for single row, `.all()` for array, `.run()` for mutations
517517+9. **Decorators required** - Must enable `experimentalDecorators` for Lit
518518+10. **Session from cookies** - Use `getSessionFromRequest(req)` to extract session ID
519519+520520+## Development Workflow
521521+522522+1. Make changes to `.ts`, `.html`, or `.css` files
523523+2. Bun's HMR automatically reloads in browser
524524+3. Write tests in `*.test.ts` files
525525+4. Run `bun test` to verify
526526+5. Check database with `bun run db:studio` if needed
527527+528528+**Never run `bun dev` yourself** - user has it running with hot reload already.
529529+530530+## Resources
531531+532532+- [Bun Fullstack Documentation](https://bun.com/docs/bundler/fullstack)
533533+- [Lit Documentation](https://lit.dev/)
534534+- [Drizzle ORM Documentation](https://orm.drizzle.team/)
535535+- [SimpleWebAuthn Documentation](https://simplewebauthn.dev/)
536536+- [Web Components MDN](https://developer.mozilla.org/en-US/docs/Web/Web_Components)
537537+- Bun API docs: `node_modules/bun-types/docs/**.md`
538538+539539+## Key Differences from Thistle
540540+541541+This project is based on Thistle but simplified:
542542+- ✅ **Drizzle ORM** instead of raw `bun:sqlite` queries
543543+- ✅ **Simpler demo** (just counter, no transcription service)
544544+- ❌ **No subscriptions** (no Polar integration)
545545+- ❌ **No email** (no MailChannels, verification codes)
546546+- ❌ **No admin system** (no roles, no admin panel)
547547+- ❌ **No rate limiting** (simplified for demo)
548548+- ❌ **No password auth** (passkeys only)
549549+550550+Focus is on demonstrating the core pattern: Bun + TypeScript + Lit + Drizzle + Passkeys.
+21
LICENSE
···11+MIT License
22+33+Copyright (c) 2025 Kieran Klukas
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+92
README.md
···4455It uses Lit, Bun, and Drizzle as the main stack and they all work together to make a wonderful combo.
6677+## How do I hack on it?
88+99+### Development
1010+1111+Getting started is super straightforward:
1212+1313+```bash
1414+bun install
1515+cp .env.example .env
1616+bun run db:push
1717+bun dev
1818+```
1919+2020+Your server will be running at `http://localhost:3000` with hot module reloading. Just edit any `.ts`, `.html`, or `.css` file and watch it update in the browser.
2121+2222+## How does it work?
2323+2424+The development flow is really nice in my opinion. The server imports HTML files as route handlers. Those HTML files import TypeScript components using `<script type="module">`. The components are just Lit web components that self-register as custom elements. Bun sees all this and bundles everything automatically including linked styles.
2525+2626+```typescript
2727+// src/index.ts - Server imports HTML as routes
2828+import indexHTML from "./pages/index.html";
2929+3030+Bun.serve({
3131+ port: 3000,
3232+ routes: {
3333+ "/": indexHTML,
3434+ },
3535+ development: {
3636+ hmr: true,
3737+ console: true,
3838+ },
3939+});
4040+```
4141+4242+```html
4343+<!-- src/pages/index.html -->
4444+<!DOCTYPE html>
4545+<html lang="en">
4646+ <head>
4747+ <link rel="stylesheet" href="../styles/main.css" />
4848+ </head>
4949+ <body>
5050+ <counter-component count="0"></counter-component>
5151+ <script type="module" src="../components/counter.ts"></script>
5252+ </body>
5353+</html>
5454+```
5555+5656+```typescript
5757+// src/components/counter.ts
5858+import { LitElement, html, css } from "lit";
5959+import { customElement, property } from "lit/decorators.js";
6060+6161+@customElement("counter-component")
6262+export class CounterComponent extends LitElement {
6363+ @property({ type: Number }) count = 0;
6464+6565+ static styles = css`
6666+ :host {
6767+ display: block;
6868+ padding: 1rem;
6969+ }
7070+ `;
7171+7272+ render() {
7373+ return html`
7474+ <div>${this.count}</div>
7575+ <button @click=${() => this.count++}>+</button>
7676+ `;
7777+ }
7878+}
7979+```
8080+8181+The database uses Drizzle ORM for type-safe SQLite access. Schema changes are handled through migrations:
8282+8383+```bash
8484+# Make changes to src/db/schema.ts, then:
8585+bun run db:push # Push schema to database
8686+bun run db:studio # Visual database browser
8787+```
8888+8989+## Commands
9090+9191+```bash
9292+bun dev # Development server with hot reload
9393+bun test # Run tests
9494+bun run db:generate # Generate migrations from schema
9595+bun run db:push # Push schema to database
9696+bun run db:studio # Open Drizzle Studio
9797+```
9898+799The canonical repo for this is hosted on tangled over at [`dunkirk.sh/tacy-stack`](https://tangled.org/@dunkirk.sh/tacy-stack)
81009101<p align="center">
···11+import { drizzle } from "drizzle-orm/bun-sqlite";
22+import { Database } from "bun:sqlite";
33+import * as schema from "./schema";
44+55+// Use test database when NODE_ENV is test
66+const dbPath =
77+ process.env.NODE_ENV === "test" ? "tacy-stack.test.db" : "tacy-stack.db";
88+99+const sqlite = new Database(dbPath);
1010+export const db = drizzle(sqlite, { schema });
1111+1212+console.log(`[Database] Using database: ${dbPath}`);
1313+1414+export default db;