Skip to content

Notes Store — Design Spec

Status: Approved by user Date: 2026-08-05 Project: notes-store (/home/piyush/code/Typescript/notes-store)

Goal

A local web app where users sign in with GitHub, create "apps" (namespaces), generate API keys per app, and store/retrieve string notes keyed by a note ID. A minimal reference embed component proves the API contract by fetching and rendering a note in a user's web app.

Architecture

A single Bun server process serving both the public API and the dashboard:

┌─────────────┐      ┌────────────────────────────────────┐
│  Embedded    │      │              Server (Bun)          │
│  component   │      │                                    │
│  (user site) │─────▶│  /api/v1/notes/*   ← API key auth  │
└─────────────┘      │  /auth/*           ← GitHub OAuth   │
                     │  /dashboard/*      ← session cookie │
                     │      ┌─────────────┴──────────┐     │
                     │      │   SQLite (bun:sqlite)   │     │
                     │      └────────────────────────┘     │
                     └────────────────────────────────────┘
  • One process, one port. Serves both the public API and the dashboard.
  • API at /api/v1/*, authenticated by Authorization: Bearer <api_key>. The key embeds the app ID; note operations are scoped to that app automatically.
  • Dashboard at /dashboard/*, protected by a signed session cookie set after GitHub OAuth login.
  • Reference component: a static JS file served at /embed.js that fetches a note via the public API and injects its content into the page.

Tech Stack

  • Runtime: Bun (bun:sqlite built-in, zero external deps for storage)
  • Language: TypeScript (strict mode)
  • Storage: SQLite via bun:sqlite, single file (e.g. data/notes.db)
  • Auth: Better Auth (GitHub OAuth + sessions; maintained successor to Lucia/arctic)
  • ORM: Drizzle (typed, SQL-first, first-party bun:sqlite driver, no codegen)
  • IDs: nanoid (URL-friendly, used for user/app/key/note IDs)
  • Dashboard: server-rendered HTML, no SPA framework
  • Lint/format: Biome (single tool)
  • Testing: bun test (unit + integration)
  • Runtime: local only for now; Docker files come later (out of scope)

Global Constraints

  • Lint and formatting are enforced via Biome + tsc --noEmit; a pre-commit hook runs lint + format + tests.
  • API keys are stored only as SHA-256 hashes. The raw key is shown exactly once, at creation.
  • The note content is a string; max length 100,000 chars.
  • The note key (when user-supplied) must match [A-Za-z0-9_-]{1,64}.
  • API error responses use the shape { "error": { "code", "message" } }.
  • No versioning of notes; updates overwrite content.
  • API keys are full-access per app (no read/write scoping) for now.
  • App ID never appears in API requests; it is embedded in the key.

Data Model

users

columntypenotes
idtext PKnanoid
github_idint, uniquefrom GitHub OAuth
github_usernametextfor display
created_attextISO timestamp

apps

columntypenotes
idtext PKnanoid
user_idtext FK → usersowner
nametextuser-visible label
created_attextISO timestamp

api_keys

columntypenotes
idtext PKnanoid
app_idtext FK → appswhich app
key_hashtext, uniqueSHA-256 of full ns_<appId>_<secret>
key_prefixtextfirst ~12 chars, for display
created_attextISO timestamp
revoked_attext nullablenull = active

notes

columntypenotes
idtext PKthe file key (user-supplied or generated nanoid)
app_idtext FK → appswhich app namespace
contenttextthe string
created_attextISO timestamp
updated_attextISO timestamp

Uniqueness: (app_id, id) — the same note key can exist in different apps without colliding; within an app it is unique.

API Design

Base path: /api/v1. All requests need Authorization: Bearer <api_key>.

Key format

ns_<appId>_<secret>
  • ns_ prefix for recognizability.
  • <appId> is a nanoid identifying the app namespace.
  • <secret> is a nanoid (~128 bits), the actual credential.
  • Stored only as SHA-256 of the full string; key_prefix holds the display prefix.

Notes CRUD

Create note

POST /api/v1/notes
Body:  { "key": "my-note" | null, "content": "Hello" }
→ 201 { "note": { "id", "content", "createdAt", "updatedAt" } }
  • key optional; omitted → generated nanoid.
  • 409 if the key already exists in this app.

Get note

GET /api/v1/notes/:key
→ 200 { "note": { "id", "content", "createdAt", "updatedAt" } }
→ 404 if not found in this app

Update note

PUT /api/v1/notes/:key
Body: { "content": "New text" }
→ 200 { "note": { ... } }

Delete note

DELETE /api/v1/notes/:key
→ 204 No Content

Auth / key resolution

  1. Parse <appId> from the Bearer token.
  2. Look up app; missing → 401.
  3. Hash full key; check api_keys for that app, ensure not revoked → 401 if bad.
  4. Note operations scoped to that app automatically.

Error format

json
{ "error": { "code": "NOT_FOUND", "message": "Note not found" } }

Codes: UNAUTHORIZED (401), NOT_FOUND (404), CONFLICT (409), VALIDATION (400), INTERNAL (500).

Apps / keys

Dashboard-only for now (no public API for app/key management).

Dashboard

Server-rendered HTML at /dashboard/*, protected by session cookie.

Auth flow (GitHub OAuth)

  1. GET /auth/login → redirect to GitHub OAuth with client ID.
  2. GitHub redirects to /auth/callback?code=....
  3. Exchange code for token; fetch GitHub user ID + username.
  4. Upsert users row (create on first login, else existing).
  5. Set signed session cookie (ns_session, HttpOnly, SameSite=Lax).
  6. Redirect to /dashboard.

Logout: POST /auth/logout clears cookie.

Pages

  • Login — "Sign in with GitHub" button when no session.
  • Dashboard home — list the user's apps.
  • App page — notes (list, create, edit, delete) + keys (create, show once, revoke).
  • No separate users page; one user per GitHub account.

Notes UI

  • List notes for the app: key + content preview.
  • Create: small form (key optional, content textarea).
  • Edit: small edit view.
  • Delete: button with confirm.
  • No pagination or search for now.

Keys UI

  • List active + revoked keys (prefix + created date).
  • "Generate key" button → modal showing raw key once with copy button.
  • Revoke button with confirm → sets revoked_at.

Reference Component

Static script at /embed.js:

html
<script src="https://our-server/embed.js"
        data-api-key="ns_abc123xyz_x7Kq2mP9vLt4Rw8s"
        data-note-key="my-note"></script>

Fetches GET /api/v1/notes/<note-key> with the API key, renders content as text into the script's host element (or a designated element). Served with no auth (it is a public static file; the API key lives in the user's page).

Error Handling

  • Consistent JSON error shape for the API.
  • Validation at the API boundary: content string (max 100k), key regex [A-Za-z0-9_-]{1,64} when provided → 400.
  • DB errors (unique constraint) → 409; unexpected → log + generic 500 (no internals leaked).
  • 401 never distinguishes "bad key" from "no key".
  • Dashboard: unauthenticated → redirect to login; forms validated server-side.

Testing

  • Unit: bun test — key parse/hash logic, validation, DB helpers.
  • API integration: server against a temp/in-memory SQLite, CRUD with a real key, statuses + bodies (happy + 401/404/409).
  • Auth: mocked GitHub token exchange (stub fetch), assert session cookie + redirect.
  • Dashboard smoke: logged-out GET /dashboard redirects to login.

Linting & Formatting

  • Biome for lint + format (single tool).
  • tsc --noEmit type gate.
  • Pre-commit hook runs lint + format + tests.
  • bun run check script runs all of the above.

Out of Scope (later)

  • Docker files / deployment.
  • Note versioning/history.
  • Read/write key scoping.
  • Public admin API for apps/keys.
  • Frontend polish of the dashboard (a frontend-design skill pass later).
  • Pagination/search on dashboard notes.
  • Multi-user sessions beyond GitHub identity (one user per GitHub account).

all ideas, ideated