A Google Sheets–style web app for tracking O-1A visa evidence. Each user creates a packet (a tiny per-user SQLite file on the server), protects it with a password, tracks evidence against the ten O-1A criteria, and shares the link + password with lawyers or friends.
Hosted at app.impossibleo1.com.
- Create packet → server generates an unguessable 128-bit ID and creates
data/packets/<id>.db(a real SQLite file per packet), seeded with the ten O-1A criteria. - Password → hashed with scrypt (salted) and stored in the packet's own SQLite file. The server never stores plaintext passwords and there is no recovery — keep it safe.
- Unlock → correct password returns a stateless 30-day HMAC token
(
AUTH_SECRETsigns it). The browser keeps it in localStorage. - Share → send someone the packet link (
/p/<id>) + the password. They get full view/edit access. - Autosave → edits debounce-save the whole sheet to the server.
- Next.js 15 (App Router, standalone output) + React 19
better-sqlite3— one.dbfile per packet underdata/packets/- No external database, no accounts, no email
npm install
npm run dev # http://localhost:3000# Required in production — signs access tokens. Without it a random secret is
# generated at data/.secret (tokens invalidate if that file is lost).
export AUTH_SECRET="$(openssl rand -hex 32)"
npm ci
npm run build
npm run start # serves on :3000Deploy on any Node 20+ host and point your reverse proxy (nginx/Caddy) for
app.impossibleo1.com at the Node process. SQLite writes to local disk, so
use a real VPS/container with persistent storage — not ephemeral serverless.
Persistence: the entire app state lives in data/ (packet .db files +
.secret). Mount it as a persistent volume and back it up. Optionally override
its location with DATA_DIR.
| Method | Route | Auth | Purpose |
|---|---|---|---|
| POST | /api/packets |
— | Create packet {name, password} → {id, token} |
| POST | /api/packets/:id/unlock |
rate-limited | {password} → {token, name} |
| GET | /api/packets/:id |
Bearer token | Read {name, tabs} (401 {locked, name} without token) |
| PUT | /api/packets/:id |
Bearer token | Replace all tabs {tabs: {<tabKey>: rows[]}} |
Each packet is a workbook of 14 tabs matching the O-1A regulatory context: one Major Award path (a one-time internationally recognized award), the 9 criterion tabs (document at least 3 — prizes/awards, press, original contributions, scholarly articles, exhibitions, leading/critical roles, high salary, judging/panels, memberships), plus logistics tabs — Expert Reviewers (recommendation letters), Final Documents (signature checklist), Costs (budget) — and an Other/Supporting tab.
Columns are typed per field: text, date (native date picker), number
(strength, min 1 / max 10), currency (USD, summed in the status bar),
select (editable combobox with suggestions), url / email (clickable
links), and checkbox (click or Space to toggle). Each tab defines its own
column schema in lib/sheet-model.ts.
Older v1 packet files (single sheet with a category column) are migrated automatically on first open: each row is routed to the tab matching its category and the category cell is dropped.
- Packet IDs are 128-bit random hex; the regex validation doubles as path-traversal protection (the id is a filename).
- Passwords: scrypt + per-packet salt, compared with
timingSafeEqual. - Unlock endpoint is rate-limited (10 failures / 5 min per IP+packet).
- Tokens are HMAC-signed, packet-scoped, 30-day expiry.
- Security headers set in
next.config.ts. - SQLite files contain the sheet rows in plaintext — the password gates API
access; it is not at-rest encryption. Treat
data/as sensitive.