astro-knots
Mode

← All ship notes

fullstack-vc 0.0.8 Published

FullStack VC v0.0.8 — Three-Bucket Stack Editor, Stacks on Turso, Google OAuth as Peer Provider

First tagged release of the FullStack VC site, accumulating three substantial feature drops since v0.0.5. The polling system that captured zero participant votes on April 29 now sits behind three peer identity providers (Google, GitHub, LinkedIn) with a link-while-logged-in flow so any provider works from any session — VCs with Workspace emails finally have a one-click path through the auth gate. Stack edits moved off the GitHub-App-commits-on-every-save pattern that the polling blueprint v2 §8.1 explicitly retired for time-sensitive data — they now persist to Astro DB on Turso in sub-second time, with a sync script that materializes back to public markdown on the operator's cadence. And the stack editor itself, which had only ever surfaced the Current bucket in its UI, is now a three-bucket drag-drop canvas with toggleable columns, cross-bucket data reshape, optimistic UI with Cmd/Ctrl+Z undo, and a mobile-friendly fallback. Six days before the May 27 Monthly All-Hands.

Why Care?

The Agentic VC Dojo's first live session on April 29, 2026 prepped seven interactive polls and captured zero participant votes despite a fully-working OAuth gate and a working Svelte poll UI. The diagnosis after the fact: the only sign-in providers were GitHub and LinkedIn. Most VC operators don't have GitHub accounts (developer demographic), and very few think to sign in with LinkedIn — they browse to it. Every fund operator has a Google Workspace email. Adding Google as a third peer provider isn't a "nice to have" for the May 27 follow-up session — it's the dominant entry point that wasn't there.

That diagnosis cascaded into the rest of this release. If identity needed Google, the stack editor needed to be reachable from any provider session (not just GitHub). If the editor was reachable, it needed to be worth the trip — which meant exposing Aspirational and Archived in the UI for the first time, and getting saves off the 30-90 second GitHub-App-commits-then-Vercel-rebuilds loop they'd been on. The three features in this release are independent technical choices, but they share one purpose: make participation in the May 27 session easier than ignoring it.

What's New Since v0.0.5

Surfacev0.0.5v0.0.8
Identity providersGitHub + LinkedInGitHub + LinkedIn + Google (all peer)
Cross-provider linkingAuto on email match onlyLink-while-logged-in — clicking a second provider attaches it to the active session's User row instead of minting a duplicate
Header auth indicator"Log in" / partial / fully-linkedTri-state with N/3 count surfaced, sign-out + stack-counts CTA in the tooltip
Stack storageparticipants/<handle>.md via GitHub App commitsAstro DB on Turso, with pnpm sync:stacks materializing back to markdown
Stack save latency30–90s (commit + Vercel rebuild)Sub-second
Stack editor UISingle-column Current-onlyThree-bucket drag-drop canvas with pill toggles, cross-bucket reshape, undo
Owner Edit affordances on public profileNoneEdit chips on head card + each tier header (Current / Aspirational / Moved on), revealed client-side via /api/me
/people/<handle>/stack/edit accessGitHub session onlyAny provider session as long as the User row's github_handle matches the URL

Key Features

1. Google OAuth as a peer identity provider

Effective v0.0.6. Google joins GitHub and LinkedIn as a first-class identity provider — no privileged status, just another way through the gate.

Why all three, not just Google: each provider tells us something different about the same person, and the value compounds when more than one is linked.

ProviderWhat it tells us
GitHubBuilding behavior — repos, languages, commit cadence. Required for the stack-write GitHub App bot.
LinkedInProfessional identity — firm, role, work history. Highest fidelity for roster + people-page cross-referencing.
Google WorkspaceDomain affiliation (hd claim), verified firm email, lowest-friction sign-in for fund operators. Future hooks for calendar / drive integrations.

The header tooltip now nudges with this framing: "We get different signal from each — connect more for richer Dojo discovery." Three states: red dot (0/3), yellow (1/3 or 2/3 with count visible), green (3/3 — "fully triangulated").

Email-verified guard: the Google callback refuses to mint a session if email_verified === false. Defense in depth for the email-as-canonical-key invariant the User row depends on.

OAuth is stateless from the provider's perspective. The original v0.0.5 callbacks would replace the active session cookie on every sign-in — meaning a user signed in via GitHub who clicked "Continue with Google" got a fresh User row with only google_sub populated, instead of attaching Google to their existing identity.

This release adds linkProviderToExistingUser() to the user-record layer. All three callbacks now check for a valid session cookie before overwriting it; if the active session is from a different provider, the new provider's sub gets attached to the existing User row's github_handle / linkedin_sub / google_sub column. Name + avatar are preserved (the user is already operating under that identity); the new email gets unioned into User.emails[].

Logged as outcome: 'provider_linked' in AuthEvent so the audit trail distinguishes a fresh signup from a link.

3. Stacks migrated to Turso with DB→markdown materialization

The polling blueprint v2 §8.1 had already retired the GitHub-App-commits-on-every-save pattern for time-sensitive data, citing commit-history pollution, PEM brittleness, latency, and concurrent-edit fragility. Stacks are editorial cadence, not real-time — they technically escape the ban. But the costs apply regardless of write frequency.

The architecture, generalizing the polling system's §9 materialization motion:

  • Authoritative store: Astro DB on Turso, one flat Stack table with a bucket discriminator column ('current' | 'aspirational' | 'abandoned') and bucket-specific nullable fields (notes / intent / reason). Unique index on (user_id, bucket, tool) — cross-bucket moves are a single UPDATE.

  • Read path: public /people/<handle> stays SSG and renders from materialized markdown. The editor reads Turso directly (SSR) so in-progress edits are visible immediately. /api/me stack counts also come from Turso so the header tooltip reflects unsynced edits.

  • Write path: /api/stack/save is now an atomic per-bucket DELETE+INSERT against Turso. Sub-second. No commit. No deploy trigger. The endpoint accepts each bucket independently — present field replaces that bucket; absent field preserves it. (Important for the bridge period before the new editor shipped; not a concern after this release since the new editor always sends all three.)

  • Sync: pnpm sync:stacks (or sync:stacks:remote) reads Turso, rewrites the three stack arrays in each participant markdown's frontmatter, preserves everything else byte-for-byte. The operator commits the result with a materialize(stacks): snapshot from Turso <ISO> convention so materialization commits are visually distinct from editorial commits in git log — solving the pollution problem we'd had since v0.0.5.

Operational footprint: new pnpm scripts migrate:stacks (one-time backfill) and sync:stacks (Turso → markdown, on demand). New scripts under scripts/ named with the _ prefix convention so they're collated with the other ops scripts. Schema push (pnpm astro db push --remote) needed once after deploy — Vercel's astro build --remote doesn't push schema (lesson learned the hard way during the Google OAuth deploy).

4. Multi-column drag-drop stack editor

The pre-release editor surfaced only the Current bucket. Aspirational and Abandoned existed in the data model and on the public profile, but the editor had no UI for them — you had to know to hand-edit the markdown frontmatter. Predictably, most participant stacks had a fleshed-out Current list and near-empty other buckets, not because people weren't trying tools and moving on from them, but because capturing that wasn't possible.

The redesigned StackBuilder.svelte (now Svelte 5 runes; the old Svelte 4 syntax is gone) makes the three buckets first-class:

  • Pill toggles for which buckets are visible. At-least-one invariant — clicking the last active pill is a no-op. Single-pill = single column at max-w 36rem; two-pill = side-by-side at max-w 60rem; three-pill snaps to two-up of the most-recently-toggled pair plus a "more →" peek of the third. State persists in localStorage per handle.

  • Drag-and-drop via svelte-dnd-action (~6KB gzipped, keyboard-accessible out of the box). Within a column, drag reorders position. Across columns (only meaningful in two-up mode), drag transforms the data shape — notes → intent when demoting Current to Aspirational, intent → reason when archiving an Aspiring tool, and so on. Same canonical mapping via a reshape() helper in stack-types.ts.

  • Inline reshape form below the dropped card: "Moved to Aspirational · the previous note became: [text] [Keep] [Edit] [Clear]". Auto-dismisses after 5s on the keep path. Nothing silent.

  • Optimistic UI with a dirty banner that surfaces the undo count. Cmd/Ctrl+Z undoes the last drag or reshape edit (LIFO depth 10, ignores keystrokes inside text fields so browser-native undo still works there).

  • Mobile fallback at ≤768px: drag handles hidden; each card gets ↑/↓ buttons for within-column reorder and a Move button that opens a modal with target-bucket radio options. Same reshape pipeline as drag — the modal pathway goes through the same code as the drag finalize handler.

  • Accessibility: SR-only live region announces every cross-bucket move ("Moved claude-code from Current to Aspiring"). svelte-dnd-action provides keyboard drag (Space to grab, arrows to move, Space to drop). prefers-reduced-motion collapses layout transitions.

5. Owner-Edit affordances on the public profile

Four small chips, hidden by default, revealed client-side when /api/me reports the viewer's handle matches the page:

  • Edit profile → on the participant head card

  • Edit → on each of the three section headers (Current stack / Aspirational / Moved on)

Works on the SSG-prerendered /people/<handle> page without sacrificing prerendering — same client-side reveal pattern the header auth indicator uses. Visitors who aren't the owner see nothing.

Bug Fixes Worth Naming

Three round-tripping fixes surfaced during end-to-end testing on production:

  • Edit gate at /people/<handle>/stack/edit had hardcoded session.provider === 'github' after Google + link-while-logged-in landed. A Google-signed-in owner with github_handle on their User row was bouncing back to the public profile. Fixed: look up the User row by whichever provider sub the active session has, then check the row's github_handle against the URL.

  • Save endpoint had the same bug independently — gate at /api/stack/save was rejecting non-GitHub sessions even when the linked User row matched. Fixed with the same by-provider-sub lookup pattern.

  • Linked-accounts panel on edit page was doing a duplicate lookup hardcoded to User.github_handle = session.subject, which fails for Google sessions (subject is the Google sub, not a github handle). Fixed: reuse ownerRow from the gate.

Pattern recorded for the future: anywhere we have a github-handle-vs-session-subject check, the right form is by-provider-sub lookup → check row.github_handle.

Operational Hand-Off

For anyone pulling this release into a similar Astro Knots site:

  1. Schema push to production Turso (Vercel's astro build --remote does NOT push schema): pnpm astro db push --remote

  2. Backfill stacks if there's existing markdown: DRY_RUN=true pnpm migrate:stacks:remote for a preview, then drop the flag to run for real

  3. Google Cloud Console redirect URI must include https://<domain>/api/auth/google/callback exactly. No wildcards — preview deployments need to be added individually or skipped (recommended for May-27-style time-pressure)

  4. Env vars on Vercel: GOOGLE_OAUTH_CLIENT_ID + GOOGLE_OAUTH_CLIENT_SECRET (in addition to the existing GitHub + LinkedIn pairs)

  5. First sync after deploy: HANDLE=<your-handle> pnpm sync:stacks:remote to confirm the round-trip produces clean markdown. Commit with materialize(stacks): snapshot from Turso <ISO> so the convention propagates.

What's Next

Two planning docs land alongside this release in context-v/tasks/:

  • Pre-create-and-Fuzzy-Bind-Users-from-External-Rosters.md — the next track on the identity layer. Zoom export → Turso User rows with emails populated → fuzzy match to public participant markdown profiles via a confidence-scored bridge (GitHub handle exact, LinkedIn URL exact, name+kauffman-class fuzz, firm match). The "claim this profile?" affordance lives on /me; an admin triage view at /admin/unbound-users handles the long tail. The privacy boundary stays clean: emails in Turso, public profiles in markdown, single handle column on User as the bridge.

  • Redesign-Stack-Builder-with-Multi-Column-Drag-Drop.md — the plan implemented in this release. Kept in the repo as living context so future contributors can see the design rationale behind the editor architecture.

For the May 27 Monthly All-Hands specifically: the three open polls are already live in production, presenters wired (Mike Moradi + Raeed Zainuddin on Claude Team for admin/audit/fundraise; Toby Rush on Obsidian 2nd Brain + OpenClaw), RSVP link wired. The polling system has the auth surface area it needed. If the conversion math is right, the next session won't repeat April 29.