--- # pages-jgc4 title: D1 schema and query layer status: completed type: feature priority: normal created_at: 2026-07-13T02:09:08Z updated_at: 2026-07-13T02:57:07Z parent: pages-77lk blocked_by: - pages-saom --- migrations/schema.sql exactly per design doc §4.2 (users, sessions, pages). src/db.rs (wasm-only): thin typed wrappers over worker d1 — upsert_user_on_login(github_id, login, avatar_url, admin_login_match) handling ADMIN_GITHUB_LOGIN auto-promotion (design §2.4), get_user_by_session(token_hash) with expiry check, create_session/delete_session, list_pages (join owner login), get_page, count_pages_for_user, insert_page (surface PK conflict as typed NameTaken error), delete_page, list_users, set_can_upload. Row structs with serde. - [x] migrations/schema.sql - [x] src/db.rs typed query layer - [x] wasm32 check + clippy pass; native suite unaffected ## Summary of Changes migrations/schema.sql (idempotent, indexed) + src/db.rs typed D1 query layer (wasm-only), by a Sonnet agent. Opus review: REQUEST-CHANGES then fixed: - BLOCKER: session expiry compare now normalizes both sides via datetime() (lexicographic ISO-T/Z trap removed); create_session param/doc contract fixed. - NIT: NameTaken detection tightened to 'unique' matches only (real FK on pages.owner_id would have misclassified as 409). Validation green on both targets; native suite unaffected (72 tests + 4 doctests).