HTML Drive
Session log · 2026-05-10 · ~6 hr
Session log · 2026-05-10 · Claude Code

From a marketplace to a personal cabinet.

A schema migration, Google SSO, a Finder-style file browser, a Docs-style doc viewer, sharing by email, and a corner ribbon — all in one session, all on a live system that started the day looking like a marketing splash page.

00TL;DR

The cabinet stopped looking like a homepage trying to sell you a tool and started being a tool — the kind you sign into, with folders, with private files, with sharing-by-email like Google Docs.

01What shipped

Six concrete things that didn't exist this morning:

Personal schema
New users, folders, shares tables. Documents and tokens get user_id. Visibility is per-doc.
commit d2c6390d
🔑
Google sign-in
teenybase's auth extension on the users table, OAuth client + secret in env, session cookie set.
commit a98b5683
🗂
Finder-style cabinet
Sidebar (locations + folders), toolbar (Connect / New folder / view switch), main pane with iframe thumbnails or list view, right-click menu.
commit a98b5683
👥
Visibility & shares
Private · Shared (by email, read or edit) · Public. Enforced on every read and version-metadata route.
commit 23e40666
📄
Docs-style viewer
Top navbar with Share + history toggle + more menu. Right rail with version history (collapsible). Iframe holds the doc; raw moves to /raw.
commit 1b21314f
Favicon & ribbon
Inline-SVG brand-mark favicon. "Make your own" diagonal ribbon on public pages, linking /agents.md.
commit 85bab1e7

02The landing page, before and after

The signature change. The cabinet's homepage stopped being a sales pitch and became its actual content: the gallery.

Before A gated splash page Hero headline, lede, "Sign in with Google" CTA, an explainer card, a small "Browse public examples →" link tucked at the bottom. No examples on screen. Felt like landing on a SaaS marketing page.
After The gallery is the page Compact top nav with brand + sign-in pill. One-line headline, one-line subhead. Then the public examples grid right there, with iframe thumbnails. Sign-in gets you a private space; you don't need it to see what the place is for.

A personal tool shouldn't pitch. It should show you what it is, and let you in.

03The doc viewer

Opening a doc used to be confusing — /d/:slug was raw HTML, and /d/:slug/about was a separate metadata page with version history. So if you wanted to read the doc and see who could access it and jump between versions, you had to navigate three places. The new viewer folds it all into one URL.

/d/your-doc · the new chrome
↩ My cabinet Your document title Public C
your HTML, rendered in an iframe

The right rail collapses to a tiny "History" pill in the corner. The chevron closes it; localStorage remembers your preference. The iframe is sandboxed (allow-scripts allow-same-origin allow-popups allow-forms) so docs that fetch their own APIs keep working.

Old /d/:slug/about URLs still work — they 301 to the new viewer. Raw HTML moved to /d/:slug/raw — that's what iframes embed and what curl/agents fetch.

04Visibility & sharing

Every doc has a visibility field — private | shared | public — and an associated shares table for per-grantee email + read/write role. Click below to walk the three states:

Visibility, in three flavors

Private

Inviting someone by email automatically bumps a private doc to shared, the way Google Docs does. The share modal is reachable from two places: in the cabinet, right-click a file → Share / visibility…, or open the doc and click Share in the top bar. Both call the same /api/me/documents/:slug/shares endpoint.

One bug bit me here, late in the session. The ACL helper read the calling user's email from c.get('auth').email — except teenybase only populates that when verified=true, and the verified flag wasn't reliably propagating. So a freshly-signed-in user who had been granted access via email saw 403 when they opened the link. Fix: pull email from the users row directly (always present once you're signed in) and case-fold both insert and lookup.

// before — auth.email was null when verified didn't propagate
const email = auth?.email || null
const email = emailFor(c, user)  // user.email > auth.email > null, lowercased

// SQL — case-fold both sides
WHERE granted_email = ?
WHERE LOWER(granted_email) = LOWER(?)

05Ping the cabinet

This page lives in the cabinet — saved via the same flow it documents. Click below to fetch its public version metadata. Same path Claude would use to remember it later.

Public versions endpoint

live
GEThttps://html.app.teenyapp.com/api/docs/session-2026-05-10-personal-cabinet/versions

This works for any reader. Reading this doc requires whatever its visibility says — but the metadata endpoint follows the same ACL, so it's transparent: if you can read the doc, you can read its history.

06Timeline

The actual order of events. Click any commit to expand.

  1. Schema migrationd2c6390d
    Added users (with auth extension on it for OAuth), folders (per-user, nestable), shares (document_id + email + can_write). Altered documents to add user_id, folder_id, visibility. Altered tokens to add user_id. SQLite did the column-add via a table-rebuild dance — fine for new rows, but the migration's INSERT INTO new SELECT FROM old dropped Thariq's article (recovered the session report from a local copy).
  2. Finder UI + Google SSO + agent flowa98b5683
    Massive rewrite of worker.ts. New homepage gates by sign-in state. Logged-in shell is a Finder: titlebar with breadcrumbs and user dropdown, toolbar with Connect/New folder + view switcher, sidebar with Locations + Folders, main pane with icon view (iframe thumbnails) and list view. Context menu on right-click (Open / View history / Copy link / Rename / Move / Share / Delete). Share modal with three radios + email invites. Agent tokens scope to user. New /api/me/* endpoints for folder + doc + share CRUD.
  3. Layout fix + landing redesign080bbbe8
    First deploy had three bugs visible in your screenshot: (1) grid-template-rows: auto 1fr for 3 children pushed the toolbar into the middle, (2) the "+ New folder" SVG had no size constraint and grew to fill its flex parent, (3) the landing page was a gated splash card. Fixed all three: auto auto 1fr, sidebar SVG sizing rule, and the gallery-first homepage you see now.
  4. OAuth route 404 — catch-all middleware fixmiddleware
    Subtle one. I'd added userApp.use('*', ...) to ensure auth was initialized for non-/api/* routes. Side effect: the wildcard middleware intercepted teenybase's /api/v1/table/users/auth/oauth/google mount, returning 404 for the entire OAuth flow. Fixed by removing the catch-all and inlining ensureAuth(c) inside currentUser() instead — same correctness, no route shadowing.
  5. Docs-style chromed viewer1b21314f
    Replaced the separate /d/:slug/about page with a unified viewer at /d/:slug. Top navbar: ← back, title, vis pill (clickable for owners), Share, history toggle, more menu (Copy link / Open raw / Rename / Delete), avatar. Iframe holds the rendered doc, sandboxed for safety. Right rail lists versions; click a row → navigate to /d/:slug/v/:n. Rail collapses to a tiny floating pill in the corner; preference stays in localStorage. /about URLs 301 to the viewer. Raw HTML now lives at /d/:slug/raw.
  6. Iframe sandbox tweakf5d879ec
    Initial sandbox was allow-scripts allow-popups allow-forms allow-modals — no allow-same-origin. That's the safer default, but it broke docs whose internal JS calls fetch('/api/...') (like the live ping demo on the previous session report). Added allow-same-origin back. Trade-off: cookies are httpOnly so still inaccessible; localStorage in the iframe can read parent's storage, but the cabinet doesn't store anything sensitive there.
  7. Share ACL email fix23e40666
    You signed in as mjsong2021@gmail.com after granting that email access — and got a 403. Why: c.get('auth').email only populates when teenybase's verified flag is true, and our mapping (verified: 'email_verified') wasn't propagating reliably. Fix: read the email from users.email via the DB lookup we already do, and case-fold both share inserts and ACL lookups. Now any signed-in user can match a granted_email regardless of casing.
  8. Favicon + "Make your own" ribbon85bab1e7
    Inline-SVG favicon: rounded-corner vermillion square with four stacked bars echoing the wordmark. Mounted at /favicon.svg (and /favicon.ico serving the same body); referenced from every siteHead(). Diagonal "⚡ Make your own" ribbon at bottom-right of the public pages, linking /agents.md — teenybase's auto-mounted clone-this hook. Same ribbon CSS shape teenyapp's empty template ships with, so visitors and AI agents that recognize the pattern can clone the project.
  9. You asked for the changelog. You're reading it.+1
    Saved via the same POST /api/save flow this changelog documents. Visibility public so it shows up in the gallery. The Ping demo above fetches /api/docs/session-2026-05-10-personal-cabinet/versions — it'll return one version (this one) the first time, then more if you re-publish.

07Known issues

1. teenybase standard CRUD list is broken on documents and versions. Returns "Could not serialize object of type PreparedQuery". The custom routes the cabinet UI uses are unaffected — homepage, browser, ACL all work. But the stock /api/v1/table/documents/list endpoint 400s. Likely a teenybase bug triggered by something in the new schema; tracking it as a deferred fix.
2. Some legacy data was lost in the first migration. SQLite rebuilt the documents table for the new columns and dropped non-essential rows in the process. Recovered the session report from a local copy; Thariq's "Unreasonable Effectiveness of HTML" v2 was lost. Future migrations carry forward more carefully.
3. teenybase auth requires auth_username and auth_password field "usages" even for OAuth-only setups. They stay NULL for Google-authed users. Annoying but harmless.

08Meta

Project
html.app.teenyapp.com
Files touched
worker.ts (rewrites), teenybase.ts (schema)
Final worker.ts size
~109 KB (was ~40 KB)
Schema tables
users · folders · tokens · documents · shares · versions
OAuth provider
google (clientId + secret in env)
Most recent commit
85bab1e71cc6 (favicon + ribbon)
Bundle
cf workers (teenybase runtime)
Saved via /api/save · live at /d/session-2026-05-10-personal-cabinet · history in the right rail