chore: remove unused files
This commit is contained in:
@@ -1,831 +0,0 @@
|
||||
# Project: `excalidraw-box`
|
||||
|
||||
## Goal
|
||||
|
||||
Build a small self-hosted web app that provides:
|
||||
|
||||
```text
|
||||
- Excalidraw editor
|
||||
- Server-side autosave to SQLite
|
||||
- No collaboration
|
||||
- No accounts inside the app
|
||||
- Reverse-proxy auth handled by Caddy
|
||||
- Manual export to PNG/SVG/.excalidraw
|
||||
- Docker Compose deployment
|
||||
```
|
||||
|
||||
The app should be intentionally small and auditable.
|
||||
|
||||
---
|
||||
|
||||
# Non-goals
|
||||
|
||||
Do **not** implement:
|
||||
|
||||
```text
|
||||
- real-time collaboration
|
||||
- public sharing links
|
||||
- user accounts
|
||||
- teams/workspaces
|
||||
- comments
|
||||
- Firebase
|
||||
- S3/MinIO in v1
|
||||
- complex permissions
|
||||
```
|
||||
|
||||
Authentication is handled outside the app by Caddy `basic_auth` or later Authelia/AuthentiK.
|
||||
|
||||
---
|
||||
|
||||
# Stack
|
||||
|
||||
Use:
|
||||
|
||||
```text
|
||||
Next.js App Router
|
||||
React
|
||||
TypeScript
|
||||
@excalidraw/excalidraw
|
||||
better-sqlite3
|
||||
Docker Compose
|
||||
Caddy reverse proxy
|
||||
```
|
||||
|
||||
Rationale:
|
||||
|
||||
- `@excalidraw/excalidraw` is the official embeddable package. ([npm][1])
|
||||
- Excalidraw exposes `onChange(elements, appState, files)` for persistence. ([Excalidraw Docs][2])
|
||||
- Excalidraw provides export utilities such as `exportToCanvas` for rendering scenes to images. ([Excalidraw Docs][3])
|
||||
- Next.js officially supports standalone Docker deployment. ([Next.js][4])
|
||||
|
||||
---
|
||||
|
||||
# Version 1 feature set
|
||||
|
||||
## V1 must have
|
||||
|
||||
```text
|
||||
1. Single default drawing
|
||||
2. Load drawing from SQLite on page load
|
||||
3. Autosave drawing to SQLite after edits
|
||||
4. Debounce saves by 1.5 seconds
|
||||
5. Show save status:
|
||||
- Saved
|
||||
- Saving…
|
||||
- Save failed
|
||||
6. Export PNG from the current scene
|
||||
7. Export .excalidraw JSON from the current scene
|
||||
8. Persist SQLite database under /data
|
||||
9. Dockerfile
|
||||
10. docker-compose.yml
|
||||
11. Caddyfile example
|
||||
```
|
||||
|
||||
## V1 should avoid
|
||||
|
||||
```text
|
||||
- multiple drawings
|
||||
- login UI
|
||||
- sharing
|
||||
- collaboration
|
||||
- migrations framework
|
||||
- Prisma, unless really needed
|
||||
```
|
||||
|
||||
Use plain `better-sqlite3` to keep the code small.
|
||||
|
||||
---
|
||||
|
||||
# Directory structure
|
||||
|
||||
```text
|
||||
excalidraw-autosave/
|
||||
app/
|
||||
api/
|
||||
drawing/
|
||||
route.ts
|
||||
globals.css
|
||||
layout.tsx
|
||||
page.tsx
|
||||
|
||||
components/
|
||||
ExcalidrawClient.tsx
|
||||
|
||||
lib/
|
||||
db.ts
|
||||
scene.ts
|
||||
debounce.ts
|
||||
|
||||
public/
|
||||
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
Caddyfile.example
|
||||
next.config.ts
|
||||
package.json
|
||||
tsconfig.json
|
||||
.dockerignore
|
||||
README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Data model
|
||||
|
||||
Use one SQLite table.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS drawings (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
For v1, always use:
|
||||
|
||||
```text
|
||||
id = "default"
|
||||
```
|
||||
|
||||
Stored `data` should be the scene object:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "excalidraw-autosave-scene",
|
||||
"version": 1,
|
||||
"elements": [],
|
||||
"appState": {},
|
||||
"files": {}
|
||||
}
|
||||
```
|
||||
|
||||
Do not store only rendered PNG. Store the editable scene JSON as the source of truth.
|
||||
|
||||
---
|
||||
|
||||
# API design
|
||||
|
||||
## `GET /api/drawing`
|
||||
|
||||
Returns the saved drawing.
|
||||
|
||||
If no drawing exists, return an empty scene:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "excalidraw-autosave-scene",
|
||||
"version": 1,
|
||||
"elements": [],
|
||||
"appState": {},
|
||||
"files": {}
|
||||
}
|
||||
```
|
||||
|
||||
## `POST /api/drawing`
|
||||
|
||||
Accepts:
|
||||
|
||||
```json
|
||||
{
|
||||
"elements": [],
|
||||
"appState": {},
|
||||
"files": {}
|
||||
}
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
```text
|
||||
- Validate body is an object.
|
||||
- Validate elements is an array.
|
||||
- Validate appState is an object.
|
||||
- Validate files is an object.
|
||||
- Strip volatile or UI-only appState fields if needed.
|
||||
- Save JSON to SQLite.
|
||||
- Upsert row id = "default".
|
||||
- Return { "ok": true, "updatedAt": "..." }.
|
||||
```
|
||||
|
||||
## Optional `GET /api/health`
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
Useful for Docker health checks.
|
||||
|
||||
---
|
||||
|
||||
# Scene handling
|
||||
|
||||
Create `lib/scene.ts`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
```text
|
||||
- define EmptyScene
|
||||
- normalize incoming scene
|
||||
- remove unwanted volatile appState fields
|
||||
- cap payload size
|
||||
```
|
||||
|
||||
Recommended payload size cap for v1:
|
||||
|
||||
```text
|
||||
25 MB
|
||||
```
|
||||
|
||||
If the scene exceeds this, return HTTP `413 Payload Too Large`.
|
||||
|
||||
Reason: Excalidraw embedded images are stored under `files`; they can make scene JSON large.
|
||||
|
||||
---
|
||||
|
||||
# Frontend behavior
|
||||
|
||||
## Page load
|
||||
|
||||
```text
|
||||
1. Render loading state.
|
||||
2. Fetch /api/drawing.
|
||||
3. Pass returned scene to Excalidraw as initialData.
|
||||
4. Render editor full-screen.
|
||||
```
|
||||
|
||||
The official integration docs show that the Excalidraw component should be used client-side in Next.js, with `"use client"` and the Excalidraw CSS import. ([Excalidraw Docs][5])
|
||||
|
||||
## Autosave
|
||||
|
||||
Use Excalidraw’s `onChange`.
|
||||
|
||||
Pseudo-flow:
|
||||
|
||||
```tsx
|
||||
onChange={(elements, appState, files) => {
|
||||
setSaveStatus("dirty");
|
||||
debouncedSave({ elements, appState, files });
|
||||
}}
|
||||
```
|
||||
|
||||
Debounced save:
|
||||
|
||||
```text
|
||||
- wait 1500 ms after last change
|
||||
- POST to /api/drawing
|
||||
- set status Saving… while request in flight
|
||||
- set status Saved after success
|
||||
- set status Save failed after failure
|
||||
```
|
||||
|
||||
Important: Do not save on every pointer movement without debounce.
|
||||
|
||||
## Save status UI
|
||||
|
||||
A small fixed indicator:
|
||||
|
||||
```text
|
||||
top-right:
|
||||
Saved
|
||||
Saving…
|
||||
Save failed
|
||||
```
|
||||
|
||||
Optionally show last saved timestamp.
|
||||
|
||||
---
|
||||
|
||||
# Export behavior
|
||||
|
||||
## `.excalidraw` export
|
||||
|
||||
Implement client-side export by taking the current scene and downloading JSON.
|
||||
|
||||
Filename:
|
||||
|
||||
```text
|
||||
drawing-YYYY-MM-DD-HHMM.excalidraw
|
||||
```
|
||||
|
||||
Content:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [],
|
||||
"appState": {},
|
||||
"files": {}
|
||||
}
|
||||
```
|
||||
|
||||
This keeps compatibility with regular Excalidraw import/export. The Excalidraw repo describes the open `.excalidraw` JSON file format as a supported feature. ([GitHub][6])
|
||||
|
||||
## PNG export
|
||||
|
||||
Use Excalidraw export utilities client-side.
|
||||
|
||||
The docs show `exportToCanvas` exported from `@excalidraw/excalidraw`; it takes `elements`, `appState`, and export dimensions. ([Excalidraw Docs][3])
|
||||
|
||||
Implementation idea:
|
||||
|
||||
```tsx
|
||||
import { exportToCanvas } from "@excalidraw/excalidraw";
|
||||
|
||||
const canvas = await exportToCanvas({
|
||||
elements,
|
||||
appState: {
|
||||
...appState,
|
||||
exportBackground: true,
|
||||
},
|
||||
files,
|
||||
getDimensions: (width, height) => ({
|
||||
width,
|
||||
height,
|
||||
scale: 2,
|
||||
}),
|
||||
});
|
||||
|
||||
const blob = await new Promise<Blob>((resolve) =>
|
||||
canvas.toBlob((blob) => resolve(blob!), "image/png"),
|
||||
);
|
||||
```
|
||||
|
||||
Then trigger browser download.
|
||||
|
||||
Do PNG export client-side first. It avoids needing headless browser rendering on the server.
|
||||
|
||||
---
|
||||
|
||||
# Important implementation detail: access to current scene
|
||||
|
||||
Use `excalidrawAPI`.
|
||||
|
||||
The Excalidraw API exposes methods such as `getFiles`. ([Excalidraw Docs][7])
|
||||
|
||||
Store latest values from `onChange` in React refs:
|
||||
|
||||
```tsx
|
||||
const latestSceneRef = useRef({
|
||||
elements: [],
|
||||
appState: {},
|
||||
files: {},
|
||||
});
|
||||
```
|
||||
|
||||
Update it inside `onChange`.
|
||||
|
||||
Use that ref for export buttons.
|
||||
|
||||
---
|
||||
|
||||
# Suggested files
|
||||
|
||||
## `package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "excalidraw-autosave",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "latest",
|
||||
"better-sqlite3": "latest",
|
||||
"next": "latest",
|
||||
"react": "latest",
|
||||
"react-dom": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "latest",
|
||||
"@types/node": "latest",
|
||||
"@types/react": "latest",
|
||||
"@types/react-dom": "latest",
|
||||
"typescript": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `next.config.ts`
|
||||
|
||||
```ts
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Database module
|
||||
|
||||
## `lib/db.ts`
|
||||
|
||||
Requirements:
|
||||
|
||||
```text
|
||||
- Read DATABASE_PATH from env
|
||||
- Default to /data/excalidraw.sqlite
|
||||
- Ensure parent directory exists
|
||||
- Initialize schema
|
||||
- Export getDrawing() and saveDrawing()
|
||||
```
|
||||
|
||||
Pseudo-code:
|
||||
|
||||
```ts
|
||||
import Database from "better-sqlite3";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const dbPath = process.env.DATABASE_PATH ?? "/data/excalidraw.sqlite";
|
||||
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
db.pragma("journal_mode = WAL");
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS drawings (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
export function getDrawing(id = "default") {
|
||||
const row = db.prepare("SELECT data FROM drawings WHERE id = ?").get(id) as
|
||||
| { data: string }
|
||||
| undefined;
|
||||
|
||||
return row ? JSON.parse(row.data) : null;
|
||||
}
|
||||
|
||||
export function saveDrawing(data: unknown, id = "default") {
|
||||
const serialized = JSON.stringify(data);
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO drawings (id, data, created_at, updated_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
data = excluded.data,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`,
|
||||
).run(id, serialized);
|
||||
|
||||
return new Date().toISOString();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# API route
|
||||
|
||||
## `app/api/drawing/route.ts`
|
||||
|
||||
Requirements:
|
||||
|
||||
```text
|
||||
- Force Node runtime, not Edge
|
||||
- GET returns saved or empty scene
|
||||
- POST saves normalized scene
|
||||
- Handle invalid JSON
|
||||
- Enforce payload size cap
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```ts
|
||||
export const runtime = "nodejs";
|
||||
```
|
||||
|
||||
Pseudo-code:
|
||||
|
||||
```ts
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDrawing, saveDrawing } from "@/lib/db";
|
||||
import { emptyScene, normalizeScene } from "@/lib/scene";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET() {
|
||||
const saved = getDrawing("default");
|
||||
return NextResponse.json(saved ?? emptyScene());
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const text = await req.text();
|
||||
|
||||
if (text.length > 25 * 1024 * 1024) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Scene too large" },
|
||||
{ status: 413 },
|
||||
);
|
||||
}
|
||||
|
||||
let raw: unknown;
|
||||
|
||||
try {
|
||||
raw = JSON.parse(text);
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Invalid JSON" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const scene = normalizeScene(raw);
|
||||
const updatedAt = saveDrawing(scene, "default");
|
||||
|
||||
return NextResponse.json({ ok: true, updatedAt });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Client component
|
||||
|
||||
## `components/ExcalidrawClient.tsx`
|
||||
|
||||
Requirements:
|
||||
|
||||
```text
|
||||
- "use client"
|
||||
- import Excalidraw and CSS
|
||||
- fetch initial data
|
||||
- render full viewport
|
||||
- onChange debounced autosave
|
||||
- export JSON button
|
||||
- export PNG button
|
||||
- save status
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
```tsx
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
```
|
||||
|
||||
Use a custom debounce instead of lodash to avoid another dependency.
|
||||
|
||||
---
|
||||
|
||||
# CSS/UI
|
||||
|
||||
Keep UI minimal.
|
||||
|
||||
```text
|
||||
- Editor fills viewport.
|
||||
- Floating toolbar at top-right.
|
||||
- Buttons:
|
||||
- Export PNG
|
||||
- Export .excalidraw
|
||||
- Save status badge:
|
||||
- Saved
|
||||
- Saving…
|
||||
- Failed
|
||||
```
|
||||
|
||||
Do not attempt to reproduce Excalidraw’s own toolbar.
|
||||
|
||||
---
|
||||
|
||||
# Docker
|
||||
|
||||
## `Dockerfile`
|
||||
|
||||
Use Next.js standalone output.
|
||||
|
||||
```dockerfile
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV DATABASE_PATH=/data/excalidraw.sqlite
|
||||
|
||||
RUN mkdir -p /data
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
## `docker-compose.yml`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
excalidraw-autosave:
|
||||
build: .
|
||||
container_name: excalidraw-autosave
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
DATABASE_PATH: /data/excalidraw.sqlite
|
||||
volumes:
|
||||
- ./data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: excalidraw-caddy
|
||||
depends_on:
|
||||
- excalidraw-autosave
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile.example:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
```
|
||||
|
||||
## `Caddyfile.example`
|
||||
|
||||
```caddyfile
|
||||
draw.example.com {
|
||||
basic_auth {
|
||||
zero REPLACE_WITH_CADDY_HASH
|
||||
}
|
||||
|
||||
reverse_proxy excalidraw-autosave:3000
|
||||
}
|
||||
```
|
||||
|
||||
Generate password hash:
|
||||
|
||||
```bash
|
||||
docker run --rm -it caddy:latest caddy hash-password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Backup plan
|
||||
|
||||
SQLite file:
|
||||
|
||||
```text
|
||||
./data/excalidraw.sqlite
|
||||
```
|
||||
|
||||
Backup command:
|
||||
|
||||
```bash
|
||||
mkdir -p ./backups
|
||||
sqlite3 ./data/excalidraw.sqlite ".backup './backups/excalidraw-$(date +%F-%H%M).sqlite'"
|
||||
```
|
||||
|
||||
Recommended cron:
|
||||
|
||||
```cron
|
||||
0 3 * * * cd /path/to/excalidraw-autosave && sqlite3 ./data/excalidraw.sqlite ".backup './backups/excalidraw-$(date +\%F-\%H\%M).sqlite'"
|
||||
```
|
||||
|
||||
Also back up `.excalidraw` exports manually for critical drawings.
|
||||
|
||||
---
|
||||
|
||||
# Security model
|
||||
|
||||
V1 app assumes:
|
||||
|
||||
```text
|
||||
- It is not public without reverse proxy auth.
|
||||
- Caddy handles TLS.
|
||||
- Caddy handles basic_auth.
|
||||
- The app does not implement users.
|
||||
- The app should not expose port 3000 to the public internet.
|
||||
```
|
||||
|
||||
In Compose, use:
|
||||
|
||||
```yaml
|
||||
expose:
|
||||
- "3000"
|
||||
```
|
||||
|
||||
Do **not** use:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "3000:3000"
|
||||
```
|
||||
|
||||
unless testing locally.
|
||||
|
||||
---
|
||||
|
||||
# Acceptance criteria
|
||||
|
||||
Codex should finish with a repo that satisfies:
|
||||
|
||||
```text
|
||||
1. `npm run dev` opens editor locally.
|
||||
2. Drawing reloads after refresh.
|
||||
3. Drawing persists after app restart.
|
||||
4. Docker Compose starts Caddy + app.
|
||||
5. SQLite DB is created under ./data.
|
||||
6. No direct public app port is exposed.
|
||||
7. Export PNG downloads a PNG.
|
||||
8. Export .excalidraw downloads editable JSON.
|
||||
9. Autosave status visibly changes.
|
||||
10. No collaboration code exists.
|
||||
```
|
||||
|
||||
Manual test:
|
||||
|
||||
```text
|
||||
1. Start app.
|
||||
2. Draw rectangle and text.
|
||||
3. Wait until status says Saved.
|
||||
4. Refresh browser.
|
||||
5. Confirm rectangle and text remain.
|
||||
6. Restart container.
|
||||
7. Confirm drawing remains.
|
||||
8. Export PNG.
|
||||
9. Export .excalidraw.
|
||||
10. Import .excalidraw into official Excalidraw and confirm it opens.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Future V2, only after V1 works
|
||||
|
||||
Add these later, not now:
|
||||
|
||||
```text
|
||||
- multiple named drawings
|
||||
- drawing list page
|
||||
- thumbnails
|
||||
- duplicate drawing
|
||||
- delete drawing
|
||||
- server-side export endpoint
|
||||
- periodic backup UI
|
||||
- WebDAV/S3 export
|
||||
- basic file manager
|
||||
```
|
||||
|
||||
For V2 multiple drawings, change routes to:
|
||||
|
||||
```text
|
||||
GET /api/drawings
|
||||
POST /api/drawings
|
||||
GET /api/drawings/:id
|
||||
PUT /api/drawings/:id
|
||||
DELETE /api/drawings/:id
|
||||
```
|
||||
|
||||
Schema:
|
||||
|
||||
```sql
|
||||
CREATE TABLE drawings (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Instruction
|
||||
|
||||
Build the V1 exactly as specified. Keep code minimal. Do not add accounts, collaboration, sharing, Firebase, Prisma, Tailwind, or extra abstractions. Use the official `@excalidraw/excalidraw` package and SQLite persistence through `better-sqlite3`. Use Caddy for auth in the deployment example.
|
||||
Reference in New Issue
Block a user