AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add deployment guide for Railway, SQLite, and production debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+143
+1
docs/site/astro.config.mjs
··· 28 28 { label: 'Seeds', slug: 'guides/seeds' }, 29 29 { label: 'OpenGraph Images', slug: 'guides/opengraph' }, 30 30 { label: 'Hooks', slug: 'guides/hooks' }, 31 + { label: 'Deployment', slug: 'guides/deployment' }, 31 32 ], 32 33 }, 33 34 {
+142
docs/site/src/content/docs/guides/deployment.mdx
··· 1 + --- 2 + title: Deployment 3 + description: Deploy hatk apps to Railway with SQLite, volumes, and production debugging. 4 + --- 5 + 6 + ## Railway 7 + 8 + ### Dockerfile 9 + 10 + Include `sqlite3` in the container for production debugging: 11 + 12 + ```dockerfile 13 + FROM node:25-slim 14 + RUN apt-get update && apt-get install -y --no-install-recommends \ 15 + ca-certificates xz-utils sqlite3 \ 16 + && rm -rf /var/lib/apt/lists/* 17 + WORKDIR /app 18 + COPY package.json package-lock.json ./ 19 + RUN npm ci 20 + COPY . . 21 + RUN npx vp build 22 + RUN npm prune --omit=dev 23 + ENV NODE_ENV=production 24 + CMD ["node", "--experimental-strip-types", "node_modules/@hatk/hatk/dist/main.js", "hatk.config.ts"] 25 + ``` 26 + 27 + ### Volume 28 + 29 + Mount a Railway volume at `/data` for the SQLite database: 30 + 31 + ```bash 32 + railway volume create -m /data 33 + ``` 34 + 35 + Set the database path in `hatk.config.ts`: 36 + 37 + ```ts 38 + export default defineConfig({ 39 + databaseEngine: "sqlite", 40 + database: process.env.NODE_ENV === "production" ? "/data/app.db" : "data/app.db", 41 + }); 42 + ``` 43 + 44 + ### Health checks 45 + 46 + If setup scripts run long imports on startup, increase the health check timeout in `railway.toml`: 47 + 48 + ```toml 49 + [deploy] 50 + healthcheckPath = "/_health" 51 + healthcheckTimeout = 600 52 + ``` 53 + 54 + ### SSH debugging 55 + 56 + Railway SSH doesn't reliably support piped stdin or shell metacharacters. Use the base64 script pattern to run queries on prod: 57 + 58 + ```bash 59 + # Write a shell script locally 60 + cat > /tmp/query.sh <<'EOF' 61 + sqlite3 /data/app.db "SELECT COUNT(*) FROM [my.collection];" 62 + EOF 63 + 64 + # Base64 encode and execute via SSH 65 + B64=$(base64 < /tmp/query.sh | tr -d '\n') 66 + railway ssh "sh -c \"echo $B64 | base64 -d | sh\"" 67 + ``` 68 + 69 + For multi-line SQL, use heredocs inside the script: 70 + 71 + ```bash 72 + cat > /tmp/query.sh <<'EOF' 73 + sqlite3 /data/app.db <<'EOSQL' 74 + EXPLAIN QUERY PLAN 75 + SELECT t.uri FROM [my.collection] t 76 + ORDER BY t.some_field DESC LIMIT 50; 77 + EOSQL 78 + EOF 79 + ``` 80 + 81 + Use bracket quoting `[table.name]` instead of double quotes for table names to avoid escaping issues. 82 + 83 + ## SQLite production notes 84 + 85 + ### Custom indexes 86 + 87 + hatk auto-creates indexes on `indexed_at DESC`, `did`, child table `parent_uri`, and child table text columns. For app-specific queries, add custom indexes in a setup script: 88 + 89 + ```ts 90 + // server/setup/create-indexes.ts 91 + import { defineSetup } from "$hatk"; 92 + 93 + export default defineSetup(async (ctx) => { 94 + const { db } = ctx; 95 + await db.run( 96 + `CREATE INDEX IF NOT EXISTS idx_plays_played_time 97 + ON "fm.teal.alpha.feed.play"(played_time DESC)`, 98 + ); 99 + }); 100 + ``` 101 + 102 + Setup scripts run on every startup. `CREATE INDEX IF NOT EXISTS` makes them idempotent. 103 + 104 + ### Datetime comparisons 105 + 106 + SQLite's `datetime()` returns space-separated format (`2026-03-16 12:00:00`) while hatk stores ISO timestamps with `T` separator (`2026-03-16T12:00:00Z`). String comparison breaks because `T` > space in ASCII. 107 + 108 + ```sql 109 + -- WRONG: matches too many rows 110 + WHERE played_time >= datetime('now', '-4 hours') 111 + 112 + -- CORRECT: generates ISO format 113 + WHERE played_time >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-4 hours') 114 + ``` 115 + 116 + ### Query performance tips 117 + 118 + Use `EXPLAIN QUERY PLAN` via SSH to diagnose slow queries. Watch for: 119 + 120 + - **`SCAN` without index** — add an index on the filtered/ordered column 121 + - **`USE TEMP B-TREE FOR ORDER BY`** — the `ORDER BY` column needs a descending index 122 + - **Large JOINs with DISTINCT** — rewrite as `EXISTS` subqueries so SQLite can walk an index and stop at `LIMIT` 123 + 124 + For expensive aggregation queries (trending lists, category counts), use stale-while-revalidate caching: 125 + 126 + ```ts 127 + let cache: { data: any; expires: number } | null = null; 128 + const TTL = 5 * 60 * 1000; 129 + 130 + async function refresh(db) { 131 + const rows = await db.query(`...`); 132 + cache = { data: rows, expires: Date.now() + TTL }; 133 + return rows; 134 + } 135 + 136 + // In handler: 137 + if (cache) { 138 + if (Date.now() >= cache.expires) refresh(db); // background refresh 139 + return ok(cache.data); // serve stale immediately 140 + } 141 + return ok(await refresh(db)); // first request waits 142 + ```