Architecture Overview

Tech Stack

LayerTechnologyPurpose
FrontendReact + Vite + TypeScriptCustomer portal
BackendSupabase (PostgreSQL + Edge Functions)Database, auth, serverless
HostingVercelFrontend deployment
PaymentsStripeBilling, subscriptions
SchedulingGoogle Calendar (native)Booking layer — service account writes to “SV Bookings”
CRMHubSpot (Free)Partnership sales pipeline
AuthSupabase Auth (Magic Links)Passwordless authentication
StorageSupabase StorageItem photos
Error MonitoringSentryPortal error capture (live Jul 11, 2026 — see Integrations)

Infrastructure

ServiceDetails
Supabase Projectgmjucacmbrumncfnnhua
Supabase URLhttps://gmjucacmbrumncfnnhua.supabase.co
Regionus-east-1
DatabasePostgreSQL 17.6.1
Vercel TeamStorageValet

Vercel Projects

ProjectURLRepo
sv-websitewww.mystoragevalet.comsv-website
sv-portal-2026portal.mystoragevalet.com (cut over Jun 10, 2026)sv-portal-2026
sv-portalsv-portal-gamma.vercel.app (v1 — decommissioned Jun 29, 2026; retired placeholder, repo archived)sv-portal (archived)
sv-wikiwiki.mystoragevalet.comsv-wiki

Development Tools

CategoryTools
RuntimeNode.js 24.14.0 LTS (Homebrew node@24, native ARM64) — npm 11.11.0 bundled
AI DevelopmentClaude Code (native installer — Homebrew cask on Mac Studio, ~/.local/bin on MacBook Air) + plugins
DatabaseSupabase CLI, Supabase MCP
HostingVercel CLI
PaymentsStripe CLI (STRIPE_API_KEY from sv-env auto-detects live mode; use env -u STRIPE_API_KEY stripe for test mode)
Analyticsgcloud CLI (Homebrew cask gcloud-cli) — Application Default Credentials for GA4 MCP
Pythonpipx (Homebrew formula pipx) — isolated Python app runner for GA4 MCP server
Secrets1Password CLI
TerminaliTerm2 + Ghostty (Ghostty is the default terminal for Claude Code work)
WorkflowRaycast (app switching, shortcuts, clipboard history, custom scripts)

MCP Servers (Claude Code Integrations)

Claude Code connects to MCP servers across three tiers — user-scoped local processes, Anthropic-managed cloud connectors, and plugin-bundled servers. These give Claude direct, tool-based access to external services without manual API calls or dashboard navigation. The bias is toward Anthropic-managed cloud connectors over self-hosted launchers: fewer moving parts, no local auth to expire, and no per-machine drift. On Jul 26, 2026 the redundant local calendly and resend launchers plus the unused figma connector were removed — Calendly and Resend remain fully available through their managed claude.ai connectors (verified same account, identical Resend domain ID), leaving only two user-scoped MCPs that have no managed equivalent.

User-Scoped MCPs (local processes, available in all Claude Code sessions)

ServerTool PrefixPurposeAuth Method
Supabasemcp__supabase__*Direct database access, schema exploration, SQL executionOAuth (remote HTTP)
GA4 Analyticsmcp__analytics-mcp__*Query website traffic, run reports, realtime visitors — read-onlyApplication Default Credentials via gcloud

Google Workspace moved to Anthropic’s official managed connectors (Gmail / Google Calendar / Google Drive, listed below) on Jun 22, 2026; the former custom mcp__google-workspace__* local MCP and its Google Cloud OAuth app were retired — see the Modernization Watchlist.

Cloud Connectors (managed by Anthropic, available in Claude Code + Desktop + Web)

ConnectorTool PrefixPurpose
Calendlymcp__claude_ai_Calendly__*List/cancel events, view invitees, manage scheduling (replaced the retired local launcher)
Canvamcp__claude_ai_Canva__*Design creation and management
Context7mcp__claude_ai_Context7__*Live API documentation lookup for any library
Dropboxmcp__claude_ai_Dropbox__*Browse/search/read Dropbox files, sharing (team files)
GitHubmcp__claude_ai_GitHub_MCP__*Repos, issues, PRs, releases, code search
Gmailmcp__claude_ai_Gmail__*Read/draft/search email
Google Calendarmcp__claude_ai_Google_Calendar__*Event management, free time, scheduling
Google Drivemcp__claude_ai_Google_Drive__*Read/search/share Drive files (Docs, Sheets, etc.)
HubSpotmcp__claude_ai_HubSpot__*CRM pipeline management (sales only)
Resendmcp__claude_ai_Resend__*Send/manage emails, contacts, domains, broadcasts — debug delivery (replaced the retired local launcher)
Sentrymcp__claude_ai_Sentry__*Error monitoring, issue triage, Seer analysis (v2 portal)
Stripemcp__claude_ai_Stripe__*Customers, subscriptions, invoices, products
Vercelmcp__claude_ai_Vercel__*Deployments, projects, build logs
Vibe Prospectingmcp__claude_ai_Vibe_Prospecting__*Business/prospect enrichment for partnership outreach (trial credits)

The Zapier connector (mcp__claude_ai_Zapier__*) was disconnected Jun 2026 — the paid annual plan went essentially unused (modern Claude models cover the automation it once bridged) and lapsed to free Jun 20, 2026. Being an account-level claude.ai connector, removing it applies across all machines automatically (unlike per-machine local skills).

Plugin MCPs (bundled with Claude Code plugins)

The context7 and supabase plugins each ship a bundled MCP, but both are shadowed and add no separate connection: Context7 is now reached through the managed claude.ai Context7 cloud connector, and the supabase plugin’s MCP is shadowed by the user-scoped supabase entry (same URL) — the plugins are kept for their skills, not a redundant MCP. Per the Supabase guardrail, never remove the user-scoped supabase entry from ~/.claude.json; removing it deletes the only working Supabase MCP (the plugin does not transparently take over).

Related: Claude in Chrome (not registered as an MCP)

Browser automation tools (mcp__claude-in-chrome__*) are provided by the Claude in Chrome browser extension, which speaks Claude Code’s extension protocol — it does not appear in claude mcp list. Used for screenshots, form filling, navigation, and reading web pages.

MCP Launcher Script Pattern

One MCP server (GA4 Analytics) uses a launcher script to inject credentials at runtime, avoiding plaintext on disk:

# Pattern: ~/Documents/storagevalet/Technology/Utility_Scripts/{name}-mcp-launcher.sh
# The script sets the credential path, then exec's the MCP server process.

# GA4: ga4-mcp-launcher.sh โ†’ sets ADC path โ†’ pipx run analytics-mcp
#
# (Resend retired its launcher Jul 8, 2026; Calendly's local launcher was
#  removed Jul 26, 2026 โ€” both now use managed claude.ai connectors.)

GA4 MCP Infrastructure

The GA4 Analytics MCP (mcp__analytics-mcp__*) provides read-only access to GA4 reports. Requires Google Cloud project storage-valet-analytics with ADC credentials. Read-only — cannot inspect admin settings (traffic rules, data filters). Full details →

Claude Code Skills

Skills are packaged units of Storage Valet expertise that Claude Code auto-discovers and loads on demand — each is a single SKILL.md file (occasionally plus support files) with YAML frontmatter describing when it applies. They inject SV-specific context (brand rules, pricing, promo codes, pipeline structure, architecture constraints) so Claude does not have to re-learn the business each session. A skill can be triggered automatically when its description matches the task, or invoked explicitly as /skill-name.

Storage Valet maintains 12 installed skill directories in ~/.claude/skills/ — the curated local set Claude Code actually reads — against 32 staged zips in Technology/claude-skills/, which is the claude.ai upload source and holds the wider library. The installed set is deliberately a subset, not drift. See the three-surface table below.

Library by function

AreaSkills
Technical / portalsv-portal-dev, sv-promo-pages, sv-web-design, sv-email-design
Brand & contentsv-brand-voice, sv-copywriting, sv-cro, sv-seo-content, sv-social-media, sv-visual-design
Sales & partnershipssv-sales-email, sv-sales-enablement, sv-partnership-outreach, sv-crm-pipeline, sv-meeting-prep, sv-competitive-intel
Growthsv-growth-experiments, sv-referral-program, sv-google-ads, sv-facebook-ads
Finance & strategysv-financial-model, sv-investor-materials, sv-quarterly-review, sv-data-analyzer
Ops / legal / CXsv-operations-sop, sv-compliance-tracker, sv-contract-reviewer, sv-customer-comms
General (non-SV)design-taste-frontend, emil-design-eng, review-animations

Canonical source & the per-machine sync trap

The canonical source of truth is the set of staged .zip files in iCloud at ~/Documents/storagevalet/Technology/claude-skills/ (each zip holds SKILL.md at its root). Skills install to ~/.claude/skills/<name>/SKILL.md.

Two-layer sync trap (confirmed Jun 25, 2026)

The iCloud zip files sync between the Mac Studio and the MacBook Air automatically. The installed ~/.claude/skills/ folder does NOT — it is per-machine, like all of ~/.claude/. So a skill edited on one Mac reaches the other as a file, but Claude Code there will not use it until it is re-extracted locally:

# Install / refresh one skill on a machine from the canonical zip:
unzip -o ~/Documents/storagevalet/Technology/claude-skills/<skill>.zip \
  -d ~/.claude/skills/<skill>/

After editing skills on either Mac, run the extract on the other to keep them in parity.

Freshness discipline: skills capture point-in-time data (pipeline counts, promo-page totals, scheduling mechanics) that drifts. Verify and patch on install rather than trusting a stale snapshot — e.g. the Jun 2026 activation corrected promo-page counts (63 → 70) and customer-scheduling references (Calendly → native portal / Google Calendar) before installing. Skills that quote CRM figures point Claude to query HubSpot live instead of hardcoding a number.

Where skills live — three surfaces, one source of truth (audited Sep 6, 2026)

Skills exist in three places that do not sync automatically. A fork here is silent: an agent simply acts on stale facts without erroring.

SurfacePathRole
Authoritative~/.claude/skills/12 real directories. Claude Code reads this. Edit here.
Codex read path~/.agents/skills/12 symlinks~/.claude/skills (made Sep 6, 2026 to end the fork).
claude.ai upload sourceTechnology/claude-skills/*.zip32 zips. Manual browser upload; there is no API path.

Codex reads ~/.agents/skills/, NOT ~/.codex/skills/ — confirmed in ~/.codex/.codex-global-state.json. ~/.codex/skills/ holds Codex’s own native skills; putting SV skills there causes duplicate discovery. Reference incident: the ~/.agents copy of sv-portal-dev told Codex for months that decommissioned v1 sv-portal was “the only ops/admin surface” and “a rollback target”, and lacked the DB-vs-UI vocabulary section.

After editing any skill, re-sync its zip (the symlinks handle Codex automatically):

cd ~/.claude/skills && zip -q -r -X ~/Documents/storagevalet/Technology/claude-skills/<name>.zip <name> -x '*.DS_Store'

Authoring rules (convergent across Anthropic’s docs, the agentskills.io open spec, and OpenAI’s own skill-creator): SKILL.md body under 500 lines — it reloads into context every turn it fires; detail belongs in bundled reference files one level deep. Frontmatter name + description only if the skill is shared with Codex, which forbids every other field (Claude Code’s ~14 extended fields are not portable). And less prescriptive procedure for stronger models, not more — Anthropic’s “degrees of freedom” rule; the default assumption is that the model is already smart, so only add what it does not already know. As of Sep 6, 2026 no vendor has published Opus-5- or GPT-6-Astra-specific skill guidance.

claude.ai upload behaviour: uploading a same-named skill replaces it in place (the confirmation shows a line diff) — no deletion, no duplicates. Custom skills do not sync between the API and claude.ai, so this stays manual browser work.

Tech-Stack Ledger & Modernization Watchlist

Last modernization review: Sep 21, 2026. This is the anchor for the weekly modernization review in the ;update daily-maintenance prompt (Part 5) and the [MODERNIZATION DRIFT] check in sv-audit. Its job: catch a custom/DIY component that an official option has since superseded — instead of letting it run unmaintained for months (as the Google Workspace custom MCP did before this review existed). Sep 21 findings: Supabase Management API's logs.all endpoint is removed 2026-09-23 — verified no SV script or repo calls it, so no action needed. Google Workspace's official MCP now also covers Docs/Sheets/Slides (beyond the Gmail/Calendar/Drive connectors already in use) — no current SV workflow needs it. analytics-mcp remains the one tracked custom/DIY item, unchanged.

The watchlist tracks every self-hosted / custom integration that should be re-evaluated whenever the vendor ships an official, managed, or better-supported alternative. The Anthropic-managed claude.ai * cloud connectors are not on the watchlist — Anthropic maintains them.

ComponentTypeWhy custom / what to watch forLast reviewed
GA4 Analytics MCP (mcp__analytics-mcp__*)Custom — pipx + ADCGoogle’s official GA4 MCP (googleanalytics/google-analytics-mcp) is still self-hosted/local + labeled experimental — no managed remote connector, so no improvement over the current launcher. No change; keep watching for a hosted version. (Jul 15 re-check: still v0.6.0, still local-only; Google Cloud’s managed-MCP directory does not list GA4.) Jul 24 re-check: still v0.6.0, still local-only — no change. Aug 18 re-check: upstream released v0.7.0 (Jul 29 — dependency bumps + a CI security fix only), still local-only with no hosted/remote mode; Google Cloud’s managed-MCP directory still does not list GA4 (checked Aug 17). No managed replacement — keep the launcher; an optional routine pipx upgrade to v0.7.0 is available but low-value. Aug 31 re-check: v0.7.0 is still the latest upstream release (no release since Jul 29), still local-only with no hosted/remote mode, and Anthropic’s connectors directory still ships no Google Analytics/GA4 connector. No change. Sep 6 re-check: still v0.7.0 (PyPI analytics-mcp, last upload Jul 29, 2026) — local-only, no hosted/remote mode, and neither Anthropic’s connectors directory nor Google Cloud’s managed-MCP directory lists GA4. No managed replacement exists. New signal this review: the user-ADC credential expired again (caught Sep 6 — the MCP reported “Connected” while every query failed 503 “Reauthentication is needed”). That recurring expiry, not the package version, is the real cost of this component; the durable fix remains a custom OAuth client ID for ADC (--client-id-file) — a service-account key is blocked by org policy constraints/iam.disableServiceAccountKeyCreation, which must stay enabled. Sep 14 re-check: still v0.7.0 (PyPI, last upload Jul 29, 2026) — no new release, no hosted/remote mode, no managed connector in either Anthropic’s or Google Cloud’s directories. No change.Sep 14, 2026

Retired (reference cases):

Open advisories (from the Jul 15, 2026 review — tracked, no action due):

Aug 31, 2026 review — checked, no action due:

Sep 6, 2026 review — checked, no action due:

Sep 14, 2026 review — checked, no action due:

Review cadence: Part 5 of ;update runs weekly (it skips if the date above is < 7 days old). Each review web-searches vendor changelogs (Anthropic, Google, Supabase, Stripe, Vercel, Node LTS) for replacements / deprecations / EOL, outputs a recommendations table (no auto-changes), and bumps the “Last modernization review” date above.

Codex ↔ Claude Code Collaboration (GPT‑6 Astra + Claude Opus 5 / Fable 5.1)

This is the standard, canonical way Storage Valet uses OpenAI Codex: as a collaborator inside Claude Code, driven from the terminal. Claude Code (Claude Opus 5 / Fable 5.1) is the orchestrator and owns the final result; it delegates specific work to Codex (GPT‑6 Astra) through the official codex@openai-codex plugin. Installed on Mac Studio May 31, 2026.

For future AI agents — read this first

If you are Claude Code (Claude Opus 5 / Fable 5.1): you may hand work to Codex for an independent second opinion, adversarial code review, rescue debugging, or a parallel background task. Use the codex-rescue subagent or the /codex:rescue command. You stay the orchestrator and integrate whatever Codex returns.

If you are Codex (GPT‑6 Astra): you are being called as a delegate from within a Claude Code session. Return focused diagnosis or implementation; Claude reconciles it and presents the result to Zach.

The terminal is irrelevant. This is an in-process CLI plugin, not a screen reader — it behaves identically in Ghostty (the default terminal for SV work) and iTerm2. Never assume a specific terminal is required for Codex collaboration.

How to invoke

MethodUse for
/codex:rescueExplicitly hand a problem to Codex — debugging, a fix request, or deeper root-cause investigation
codex-rescue subagentClaude invokes Codex proactively when stuck or when a second implementation/diagnosis pass adds value
Natural language“Get a Codex second opinion” / “Have Codex review this” — Claude routes to the subagent
/codex:setupVerify the runtime is ready. --enable-review-gate forces a Codex review before each stop (off by default)

What Codex contributes

Setup & key facts

Marketplaceopenai/codex-plugin-cc (GitHub-verified OpenAI org)
Plugincodex@openai-codex v1.0.4
RuntimeLocal Codex CLI, authenticated via ChatGPT login; the shared runtime starts on demand on the first review/task
Data boundaryCode and context are sent to OpenAI whenever Codex is invoked. Approved for SV code. Never route secrets (op / .sv-session values) through it.
MachinesInstalled on Mac Studio. Plugins do not sync between machines — the MacBook Air needs the same install before Codex is available there.

Install (per machine) — run these inside Claude Code:

/plugin marketplace add openai/codex-plugin-cc
/plugin install codex@openai-codex
/reload-plugins
/codex:setup   # wires to the local Codex CLI; sign in with the ChatGPT account if prompted

Do not confuse with ChatGPT “Work with Apps”

That is a separate, unrelated feature where the ChatGPT desktop app passively reads the terminal screen via macOS Accessibility. It supports iTerm2 / Terminal / Warp but not Ghostty, and is not how SV collaborates on code. The Codex plugin above is the chosen path precisely because it is terminal-agnostic and makes Codex an active participant rather than a screen-watcher — which is what lets Ghostty stay the default terminal.

Repository Structure

Integrated System (two portals + shared backend)

These repos share the common Supabase backend (project gmjucacmbrumncfnnhua) and must be considered together:

~/code/sv-portal-2026  # LIVE customer portal (Vite 8 / React 19) โ€” native Google Calendar; cut over Jun 10, 2026
~/code/sv-portal       # v1 portal (Vite 5 / React 18) โ€” DECOMMISSIONED Jun 29, 2026 (frontend retired, repo archived)
~/code/sv-edge         # Supabase Edge Functions (Deno/TypeScript) โ€” shared by both portals
~/code/sv-db           # Database migrations (SQL) โ€” shared by both portals
~/code/sv-docs         # Ops scripts, runbooks, archives (orchestrates all 7 repos)

Cutover executed Jun 10, 2026 (~20:33 ET): portal.mystoragevalet.com now serves the sv-portal-2026 codebase — the Vercel domain was re-pointed per DEC-020, with the founder waiving the remaining checklist items (signup E2E, Manage-Billing verify, $2,000-cap verify; now post-flip tasks). The first live-domain booking was verified end-to-end the same evening: DB custody + audit trail + Google Calendar event. v1 (sv-portal) was decommissioned Jun 29, 2026 (DEC-021): frontend retired, v1-only edge functions + RPCs + views removed, repo archived. Ops now lives in the v2 portal’s staff /ops surface (W1.4).

Both portals run against the same Supabase project. The v2 backend (additive) was merged and applied to prod on Jun 9, 2026; native Google Calendar scheduling is the live booking layer on the customer domain (DEC-0001 executed): service account sv-calendar@sv-scheduling.iam.gserviceaccount.com writes to the founder-owned “SV Bookings” calendar (secrets GOOGLE_SERVICE_ACCOUNT_JSON + GOOGLE_CALENDAR_ID set in Supabase). Availability comes from get-available-slots (plan service windows ∩ 48-hour lead ∩ calendar FreeBusy), with bookings available up to 365 days out via the month-paged slot picker. Calendly remains only as v1’s legacy read-only webhook path.

Marketing & Docs Sites

RepoPurposeURL
sv-websiteMarketing site, signup flow, 73 promo pageswww.mystoragevalet.com
sv-wikiInternal docs (AI + human reference)wiki.mystoragevalet.com

sv-website is not standalone — its signup/partnership forms call three sv-edge functions (signup-webhook, create-checkout-trial, partnership-inquiry) on the shared Supabase project, so changing those contracts breaks the site. Only sv-wiki is fully standalone (no integrations).

Customer Portal (sv-portal-2026 — LIVE)

Overview

Customer Surfaces (6)

SurfacePurpose
LoginMagic link authentication
DashboardInventory overview + booking management
Items + Add ItemItem catalog; photos-first item creation
Item DetailSingle-item view — schedule, reschedule, revert, remove
BookingItems-first scheduling flow (native Google Calendar slots)
AccountProfile, billing, settings

Per-item remove on Manage (F5, Jul 2026): the Dashboard Manage view exposes a per-item “Cancel Pickup/Delivery” action that removes a single item from a pending visit via the same revert RPC path as Item Detail; removing the last active item from a visit cancels the visit itself.

Staff operations live in the v2 portal’s /ops surface (shipped W1.4) — staff-gated (is_staff()) and AAL2-stepped-up, reading fn_v2_ops_queue / fn_v2_ops_booking_detail and acting through the staff edge wrappers (complete-service, staff-booking-cancel, staff-booking-reschedule, staff-item-revert).

v1 (sv-portal) — Decommissioned

v1 was decommissioned Jun 29, 2026 (DEC-021): frontend retired (static placeholder at sv-portal-gamma.vercel.app, git disconnected), its v1-only edge functions + RPCs + views removed from prod, repo archived. Its former staff routes (/ops, /admin/waitlist, /admin/customers) and the get_ops_actions RPC behind them no longer exist — ops moved to the v2 /ops surface above.

Commands

cd ~/code/sv-portal-2026
npm run dev          # Vite dev server (mock mode by default โ€” every surface browsable)
npm run build        # tsc -b && vite build (must pass before any push)
npm run typecheck    # tsc -b
npm run test         # vitest run

cd ~/code/sv-portal  # v1 โ€” DECOMMISSIONED Jun 29, 2026 (archived; reference only, not deployed)

sv-edge (Edge Functions)

Overview

Functions (20 active)

Scheduling & v2 Booking (Google Calendar)

FunctionPurposeTrigger
get-available-slotsAvailability: plan service windows ∩ 48-hour lead ∩ calendar FreeBusyPortal booking flow
v2-booking-confirmConfirm booking; create “SV Bookings” calendar eventPortal
v2-booking-cancelCancel booking; revert items; delete calendar eventPortal
v2-booking-rescheduleMove booking to a new slot; update calendar eventPortal
v2-booking-revertPer-item revert off a scheduled bookingPortal

Stripe Integration

FunctionPurposeTrigger
create-checkout-trialStripe checkout with a complimentary first month, 30-day trial (LIVE). Returns HTTP 409 for an existing member (trialing/active/past_due) to prevent double-billing (B1, Jun 15, 2026).Landing page
create-portal-sessionStripe billing portalPortal
stripe-webhookProcess Stripe eventsWebhook
billing-previewNext-charge preview for the Account card — reads the member’s upcoming invoice via Stripe invoices/create_preview so displayed amounts include discounts/credits (Jul 14, 2026)Portal
referral-codeMints/reads the member’s personal referral promotion code on coupon REFERRAL100 ($100 off first month) — W4 referral Phase 1 (Jul 10, 2026). Coupon not found in live Stripe on Sep 7, 2026, so this function cannot mint a working code; the referee reward is also worth $0 while month one is complimentary, and what it becomes is an open founder decision.Portal

Calendly (Legacy v1 Path)

FunctionPurposeTrigger
calendly-webhookLegacy v1 read-only path (HMAC verification ENABLED, fails closed) — not the live booking layerWebhook

Booking Operations

FunctionPurposeTrigger
complete-serviceMark service completedOps UI
staff-booking-cancelStaff wrapper: cancel a booking from /ops (AAL2-gated)Ops UI
staff-booking-rescheduleStaff wrapper: reschedule a booking from /ops (AAL2-gated)Ops UI
staff-item-revertStaff wrapper: per-item revert from /ops (AAL2-gated)Ops UI

Customer & Email

FunctionPurposeTrigger
signup-webhookPre-register, check service areaLanding page
partnership-inquiryPartnership form submissionsWebsite
send-emailTransactional emails via ResendSystem
resend-webhookResend delivery-event webhook (bounce/delivery tracking)Webhook

Utility

FunctionPurposeTrigger
health-checkReturns status + timestamp (edge function health probe)System

Deployment (CRITICAL)

โš ๏ธ ALWAYS use --no-verify-jwt flag for webhooks

cd ~/code/sv-edge

# Individual function
supabase functions deploy stripe-webhook --no-verify-jwt
supabase functions deploy calendly-webhook --no-verify-jwt

# Or deploy all customer-facing functions (recommended)
./scripts/deploy-customer-facing.sh   # 19 functions, all with --no-verify-jwt
./scripts/smoke-edge.sh               # post-deploy smoke check

Webhook URLs

WebhookURL
Stripehttps://gmjucacmbrumncfnnhua.supabase.co/functions/v1/stripe-webhook
Calendlyhttps://gmjucacmbrumncfnnhua.supabase.co/functions/v1/calendly-webhook

Incident Log

DateIncidentRoot CauseResolution
Feb 18-19, 2026 update-booking-items and booking-cancel returning CORS errors and HTTP 401 from portal. Item attachment and booking cancellation completely blocked for customers. Two bugs: (1) CORS preflight Access-Control-Allow-Headers missing apikey and x-client-info — browser blocked POST after OPTIONS. (2) Auth called getUser() without token argument on a stateless Supabase client, so JWT was never verified (always 401). Commit a32c6d3: added apikey, x-client-info to CORS headers; switched to getUser(token) pattern (matching create-portal-session). Both functions redeployed. Verified via production logs: POST 200 on v44/v16.
Feb 11-15, 2026 stripe-webhook returning HTTP 500 for all event types. 6 unique events failed (34 total retry attempts). 1 subscription stuck as past_due. Two overloaded versions of update_subscription_status existed in the database (one accepting text, one accepting subscription_status_enum). PostgREST could not disambiguate (error PGRST203). Created by Nov 25 + Jan 7 migrations using CREATE OR REPLACE with different param types (PostgreSQL treats as separate functions). Migration 20260215000001 drops the subscription_status_enum overload, leaving only the text version. No edge function changes needed. Failed events replayed from Stripe Dashboard.

Lesson Learned: Function Overloading + PostgREST

PostgreSQL's CREATE OR REPLACE FUNCTION does NOT replace a function when the parameter types differ — it creates a second overload. PostgREST cannot disambiguate overloaded functions with the same parameter names but different types. Always DROP FUNCTION the old signature before creating a new one with different parameter types.

Promo Page Architecture

Property-Specific Promo Landing Pages

73 property-specific promo pages are served as generated static files (sv-website/promo/{slug}.html), each derived from the canonical template sv-website/promo/index.html by promo/generate.py (stdlib Python, run manually, output committed). The PROPERTIES map inside the template remains the single source of truth. Generation exists so each page carries a property-specific <title>, meta description, Open Graph tags, and canonical URL — before Jul 21, 2026 every share preview and search result was identical and generic. See Partnerships > Promo Landing Pages for the business context.

How Routing Works

Vercel serves the filesystem first: /promo/beacon matches the generated promo/beacon.html (via cleanUrls: true). A rewrite rule in sv-website/vercel.json remains as the fallback for anything with no generated file:

{ "source": "/promo/:code", "destination": "/promo" }

The fallback serves the template for unknown slugs and for /promo/sample (deliberately never generated). On every page — generated or fallback — client-side JavaScript:

  1. Reads the URL slug (e.g., beacon from /promo/beacon)
  2. Looks it up in the PROPERTIES object embedded in the HTML (74 entries = 73 real + sample)
  3. If found: populates the page with the property name, promo code, share links, and QR code
  4. If not found: hides the promo content and shows a “Page Not Found” message with a link back to the main site

File Structure

FilePurpose
sv-website/promo/index.htmlCanonical template + the PROPERTIES map (inline CSS + JS) — the single source of truth
sv-website/promo/{slug}.html73 generated pages (property-specific head) — never hand-edit; regenerate instead
sv-website/promo/generate.pyGenerator + --check drift guard (runs in sales-engine-check.sh)
sv-website/vercel.jsonFallback rewrite: /promo/:code/promo
sv-docs/guides/promo-landing-pages.mdComprehensive operational guide

Design Characteristics

Analytics Events

Promo pages fire events to both Vercel Web Analytics and GA4:

Event NameTriggerData
promo_page_viewPage loads with a valid slugslug, property, code
promo_share_copyUser clicks “Copy to clipboard” on the share snippetslug
promo_link_copyUser clicks the copy icon on the direct linkslug

Events use the window.va?.('event', ...) script-tag API pattern (not .track()). GA4 captures page views and scroll depth automatically via Enhanced Measurement.

QR Code Generation

QR codes are generated dynamically via api.qrserver.com (external API). Each encodes the full promo URL (e.g., https://mystoragevalet.com/promo/beacon). The QR image uses Midnight (#0A1628) for brand consistency with the Operation Midnight Teal palette. Suitable for lobby flyers, elevator screens, move-in packets, and digital signage.

Dual-View: Resident vs Property Manager

Promo pages support two view modes controlled by a URL parameter:

ViewURLBehavior
Resident (default)/promo/beaconClean page with property name, promo code, and signup CTA. Share tools are hidden.
Property Manager/promo/beacon?pm=trueAdds “Share with Residents” section: copy-paste email snippet, direct link with copy button, and printable QR code for lobby signage.

When PM mode activates, the ?pm=true parameter is stripped from the URL bar via history.replaceState so the clean URL can be shared, and the mode is persisted in localStorage so a reload or bookmark on the PM's own browser keeps the toolkit (added Jul 21, 2026 — previously a reload lost it permanently). The toggle is client-side — the .share-section element is hidden by default and displayed only when isPM is true. PM share outputs carry real attribution tokens: the copied email snippet links with ?src=email, the copy-link button with ?src=link, and the QR code with ?src=pm-qr; a visit with no token attributes as direct. (An earlier version of this page documented a ?src=pm token that the code never emitted.)

Slug Convention

Adding a New Property (Technical Steps)

  1. Create a Stripe promotion code on coupon W8K6JWCH (LIVE mode — confirm with Zach):
    stripe promotion_codes create \
      -d coupon=W8K6JWCH \
      -d code=NEWPROPERTY \
      -d metadata[property_slug]=newproperty \
      -d metadata[property_name]="New Property Name"
  2. Add to the PROPERTIES object in sv-website/promo/index.html (alphabetical by slug):
    "newproperty": { name: "New Property Name", code: "NEWPROPERTY" },
  3. Regenerate the per-property pages: python3 promo/generate.py (creates promo/newproperty.html and updates the sitemap).
  4. Commit and push the template plus the regenerated files to main. Vercel auto-deploys within ~60 seconds.
  5. Verify: Visit mystoragevalet.com/promo/newproperty and confirm property name, promo code, QR code, and share tools render correctly.

Removing a Property

  1. Remove the entry from the PROPERTIES object in promo/index.html
  2. Run python3 promo/generate.py — it deletes the orphaned generated page and updates the sitemap
  3. Deactivate the corresponding Stripe promotion code
  4. Commit and push — the URL will then show the “Page Not Found” fallback

Planned Enhancements

Future phases (documented in the archived spec, sv-docs/archive/2026-06-05-cleanup/promo-landing-pages.md) would add:

Integrations

Stripe

ItemValue
ModeLIVE
Product$299/month subscription
TrialComplimentary first month (30 days) on all new subscriptions
Setup FeeNone (eliminated)
Webhook Versionv4.1
Events Processedcheckout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.trial_will_end, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed
Idempotency & RecoveryClaim-first dedup; on handler failure the claim is released (release_stripe_webhook_event_v2, migration 20260612233000) so Stripe’s retry re-processes, and an ops alert fires (Jun 14, 2026). A $0-invoice guard skips payment stamping on the trial-activation invoice so trial members are not mis-marked as paid.
Ops AlertsfireOpsAlert on payment failure, membership cancellation (customer.subscription.deleted), and unresolved-profile billing events (Jun 15, 2026).
Duplicate-Bill GuardAn existing member re-running the unauthenticated signup is blocked (HTTP 409) before a second subscription is created; a belt-and-suspenders check in the webhook alerts ops if one ever slips through (B1, Jun 15, 2026).
Next-Charge PreviewThe portal Account card shows the member’s real upcoming charge (discounts/credits included) via the billing-preview edge function — POST /v1/invoices/create_preview with API version pinned 2025-03-31.basil (the old GET /v1/invoices/upcoming is deprecated). Falls back to the plan price/read-model label if the preview is unavailable (Jul 14, 2026).
2025+ Payload ShapeOn 2025+ Stripe API versions, subscription.current_period_end lives on the subscription items — the webhook reads it with an items.data[0].current_period_end fallback. (Root-caused Jul 14, 2026: the top-level field was null so customer_profile.current_period_end was never stamped; fixed + backfilled.)
Member ReferralAs designed: each member gets a personal promotion code (e.g. ZACH-7K4M) on shared coupon REFERRAL100 ($100 off first month, duration: once, first-time transactions only), minted lazily by referral-code and stored in public.referral_codes; the share link www.mystoragevalet.com/?code=CODE#signup auto-applies it at checkout. Not live: REFERRAL100 was not found in the live Stripe account on Sep 7, 2026, and a first-month discount is worth $0 under the complimentary first month, so the referee reward is an open founder decision. The referrer-side manual $100 account credit (Phase 1) is unaffected.

Google Calendar (Native Scheduling — LIVE)

The live booking layer on the customer domain since Jun 10, 2026 (DEC-0001 executed). Booking confirm creates the calendar event; cancel/revert deletes it. Custody never mutates without a calendar event (block-on-outage).

ItemValue
Calendar“SV Bookings” (Google, founder-owned)
Service Accountsv-calendar@sv-scheduling.iam.gserviceaccount.com
SecretsGOOGLE_SERVICE_ACCOUNT_JSON + GOOGLE_CALENDAR_ID (Supabase secrets)
Availabilityplans.service_windows ∩ 48-hour lead ∩ calendar FreeBusy (via get-available-slots)
Booking HorizonUp to 365 days (month-paged slot picker)
Signal vs TruthThe calendar is signal; the database stays canonical

Calendly (Legacy — v1 read-only path)

Calendly is no longer the live booking layer. The calendly-webhook function remains deployed as a legacy webhook path (kept past the v1 decommission, pending a separate Calendly-retirement decision) with HMAC verification ENABLED (fails closed — keep CALENDLY_WEBHOOK_SIGNING_KEY set in Supabase secrets). Events: invitee.created, invitee.canceled. Webhook only, no outbound API calls.

Supabase Storage

ItemValue
Bucketitem-photos (private)
AccessSigned URLs (1-hour expiry)
Max Size20MB per file
Allowed Typesimage/jpeg, image/png, image/webp
HEIC HandlingiPhone HEIC/HEIF library photos are transcoded to JPEG on-device before upload (imageTranscode.ts, I1 Jun 15, 2026) so they render outside Safari; the bucket allowlist stays JPEG/PNG/WebP (HEIC is never stored).
Max Photos5 per item

Transactional Email (Resend)

All transactional mail renders via the send-email edge function (Resend). Customer-facing types, plus the internal ops_alert:

TypeSent on
welcomeNew member (checkout completed) — carries the magic sign-in link
booking_confirmedBooking confirmed (with .ics + Google Calendar link)
booking_rescheduledBooking moved (monotonic ICS SEQUENCE updates the saved event)
booking_canceledBooking canceled OR last item reverted off a booking; attaches a METHOD:CANCEL invite (E3/E4, Jun 15, 2026)
trial_ending~3 days before trial end; a no-card variant tells card-less members to add one (E6)
membership_canceledMembership ended; branches on items-in-storage to prompt the complimentary final delivery (Jun 15, 2026)
payment_failedFailed charge (member now past_due)
ops_alertInternal — routed to OPS_ALERT_EMAIL (scheduler/calendar/billing anomalies)

Deliverability (Jun 15, 2026): every customer email send is wrapped in EdgeRuntime.waitUntil across all 7 sending functions, so the Supabase isolate cannot freeze before the Resend POST completes (previously these fire-and-forget sends could silently never deliver). send-email also fires an ops_alert when a customer send returns non-200. Booking emails + their ICS now include the service address (LOCATION).

Website Analytics

GA4 (G-9SBYJ8QEC1) and Vercel Web Analytics Plus ($10/month) run on all sv-website pages. GA4 handles behavioral analytics, acquisition channels, and UTM attribution. Vercel handles custom event tracking and privacy-friendly visitor counts. Vercel Speed Insights tracks Core Web Vitals.

Full analytics documentation → — configuration, internal traffic filtering, custom events, UTM attribution, and GA4 MCP tools.

Error Monitoring (Sentry)

Sentry error monitoring is LIVE on the v2 portal since Jul 11, 2026 — org my-storage-valet-llc (us.sentry.io), project sv-portal-2026.

HubSpot CRM

Added Mar 2026. Partnership sales pipeline management โ€” tracks prospective property partners from research through signed partnership. Separate from product infrastructure (Supabase, Stripe, Google Calendar handle customer-facing operations).

ItemValue
Account ID245438948
PlanFree (Sales Hub)
Pipeline“Property Partnerships” (internal name: default)
Records~225+ deals (prospective partners); company/contact counts grow with ongoing enrichment — query HubSpot live for current totals
Chrome ExtensionHubSpot Sales for Gmail (installed Mar 8, 2026)
Email IntegrationGmail — emails sent from Gmail, tracked/logged by HubSpot
Claude Code AccessHubSpot MCP tools (mcp__claude_ai_HubSpot__*)

Pipeline Stages

StageInternal IDProbability
Research331310254810%
Outreach Sent331310254920%
Follow-up331310255030%
Meeting Scheduled331310255150%
Negotiation331310255275%
Signed Partnershipclosedwon100%
Not Interestedclosedlost0%

Custom Deal Properties

property_address, unit_count, property_type, city, outreach_status, data_source, date_sent, followup_date, response_date, promo_code, promo_url, activity_log

Scope & Boundaries

E-Signature & Contracts (Jun 2026)

Go-to stack for generating and sending Storage Valet agreements (partners, customers, vendors). Decided Jun 12, 2026: Google Workspace eSignature is primary; PandaDoc Free eSign is the backup for recipient preference or outages. No additional paid tool. Full step-by-step workflow → Legal § Contracts & E-Signature.

ItemValue
PrimaryGoogle Workspace eSignature — included in the Business Standard plan ($0 incremental); 200 requests/user/month; up to 10 signers/doc; works on Google Docs and Drive PDFs; recipients need no Google account; auto-generates signed PDF + audit trail
BackupPandaDoc Free eSign (zach@mystoragevalet.com) — 60 sends/year, 2 recipients/doc, 5 templates, no integrations, no hyperlinks inside sent docs
Retained cloud workflow homeGoogle Drive → Storage Valet — Contracts/ (01 Templates / 02 Out for Signature / 03 Executed; see its _README.txt). Use manual web access; existing cloud objects and sharing remain in place after desktop sync retirement.
Single-archive ruleEvery executed contract is filed in 03 Executed regardless of which tool collected the signature
Where it livesOpen the PDF/Doc in Drive preview → Request eSignature toolbar button (or the ⋮ menu). NOT in Drive’s right-click “Open with” menu. Verified in-product Jun 12, 2026 on the zach@ account — no admin enablement was needed

SEO & Performance (Feb 2026)

ItemDetail
JSON-LD Structured DataOrganization schema on all 6 main pages; LocalBusiness on index; FAQPage on faq. Enables Google rich snippets.
Canonical URLs<link rel="canonical"> on all 6 main pages. No trailing slash (matches cleanUrls: true).
Cache Headers (vercel.json)Images: 1 year immutable. CSS: max-age=0, must-revalidate (browser always revalidates; Vercel returns 304 if unchanged). Favicon: 1 year immutable.
WCAG Text ContrastBody text uses full-opacity Mist (#8BA4A8) on Abyss/Midnight in dark mode and full-opacity Storm (#3D4F54) on Cloud/White in light mode. Canonical v6.2 secondary/meta text uses the named --color-muted role (Mist at 84% / Storm at 88% via color-mix). Arbitrary element opacity and ad hoc secondary-text values are prohibited — see Brand §08.
Skip-to-ContentVisually-hidden skip link on all 6 main pages (WCAG 2.4.1 Level A).
robots.txtPermits all crawlers. Points to /sitemap.xml.
sitemap.xmlLists all 8 HTML pages with <priority> and <lastmod> dates. Auto-deployed via Vercel.
Heading HierarchyFooter headings converted from <h4>/<h5> to <p class="footer-heading"> on all pages. Eliminates skipped heading levels flagged by Lighthouse without changing visual appearance.
Main LandmarkAll subpages (terms, privacy, faq, testimonials, partnerships, signup/*) wrapped in <main> element. Landing page already had one. Enables screen reader "skip to main content" navigation.
Footer ContrastFooter uses Mist (#8BA4A8) text on Midnight (#0A1628) background — 6.9:1 AA. All text/link contrast ratios verified against the Operation Midnight Teal accessibility table (Brand §08).
Lighthouse ScoresAccessibility 100/100 on all pages (up from 93 landing, 91 partnerships). Best Practices 100/100 after adding favicon.ico.

Security

Authentication

Row Level Security (RLS)

All customer tables are protected by RLS:

Billing Protection

Billing fields on customer_profile are protected:

Secrets Management

Policy (Non-Negotiable)

Never paste secrets into chat, docs, or commits. Never ask a human to paste secrets into chat. 1Password CLI (op) is the approved source of truth for all credentials.

Secret Inventory

Secret1Password ItemFieldConsumers
Supabase DB PasswordSupabase ProductionpasswordLocal psql only
Stripe Live Secret KeyStripe Livesecret_keySupabase secrets (STRIPE_SECRET_KEY), local Stripe CLI
Stripe Webhook SecretStripe Livewebhook_secretSupabase secrets (STRIPE_WEBHOOK_SECRET)
Resend API KeyResend APIcredentialSupabase secrets (RESEND_API_KEY) for the send-email edge function. (No longer used by the Resend MCP — that migrated to OAuth Jul 8, 2026.)
GA4 OAuth Client IDGoogle Cloud - GA4 Analytics MCPclient_idGA4 MCP (via ga4-mcp-launcher.sh; ADC at ~/.config/gcloud/application_default_credentials.json)
Calendly PATCalendlyPATManual Calendly API calls only. The local MCP launcher that used it was removed Jul 26, 2026; the managed claude.ai Calendly connector uses OAuth, not this PAT. Candidate for retirement at Calendly sunset.
Calendly Webhook Signing KeyCalendlywebhook_signing_keySupabase secrets (CALENDLY_WEBHOOK_SIGNING_KEY)
Supabase Service Role KeySupabase Productionservice_role_keySupabase-managed; used by all edge functions

Approved Patterns

# Fetch a secret (never prints to terminal)
OP_BIOMETRIC_UNLOCK_ENABLED=true op read 'op://Storage Valet/Stripe Live/secret_key'

# Push to Supabase without exposing the value
OP_BIOMETRIC_UNLOCK_ENABLED=true op read 'op://Storage Valet/Stripe Live/secret_key' \
  | xargs -I {} supabase secrets set STRIPE_SECRET_KEY="{}" --project-ref gmjucacmbrumncfnnhua

# Shell env vars โ€” lazy-loaded via sv-env function (run once per session)
# Secrets are NOT resolved at shell startup to avoid macOS permission popups
sv-env         # loads SUPABASE_ACCESS_TOKEN, STRIPE_SECRET_KEY, RESEND_API_KEY
               # also caches to ~/.sv-session (chmod 600) for sub-agent inheritance
               # session file PERSISTS across shells; run sv-env-clear to wipe manually

Never Do This

Never run secrets inline in commands (e.g., PGPASSWORD="actual_password" psql ...). Claude Code's permission caching stores the literal command string, persisting the secret to disk. Always use op read or $ENV_VAR references instead.

Rotation Procedures

  1. Generate/rotate the credential in the service's dashboard
  2. Update the value in 1Password (Storage Valet vault)
  3. If the secret is in Supabase secrets, push via: op read '...' | xargs -I {} supabase secrets set KEY="{}"
  4. Verify the dependent service still works (e.g., test checkout, test email)

Deferred: Supabase service role key rotation. High blast radius (breaks all 20 edge functions). Requires planned maintenance window. See DEC-011.

Tripwire (Automated Detection)

sv-final-audit.sh includes a secrets tripwire that scans ~/.zshrc, ~/.claude/settings.json, and ~/.claude/settings.local.json for plaintext credential patterns at the end of every session. It also flags any .env files in ~/code/ repos. Additionally, the audit script includes a GitHub hygiene layer that detects stale PRs (open >7 days) and orphan remote branches (>30 days with no associated PR) across all 7 repositories.

Deployment

Portal (Vercel)

cd ~/code/sv-portal-2026
npm run build        # tsc -b && vite build โ€” must pass before any push
vercel               # Preview deploy (NOT --prod)

sv-portal-2026 auto-deploys from its GitHub main branch; the customer domain has pointed at it since Jun 10, 2026. AI commits, pushes, and merges PRs to main directly to ship โ€” no manual promotion step. Do not run vercel --prod directly.

โš ๏ธ There is no frontend rollback target

v1 (sv-portal) was decommissioned Jun 29, 2026 and its Vercel project now serves only a static “portal has moved” placeholder that links back to the live portal. Re-pointing portal.mystoragevalet.com at it would cause an outage, not a rollback — and its v1-only edge functions, RPCs and views were removed from prod, so there is no working backend behind it either. Roll forward instead: promote a known-good sv-portal-2026 deployment in Vercel, or revert the offending commit and push. Corrected Jul 28, 2026; the same stale instruction was removed from sv-db/CLAUDE.md the same night.

PR validation: there is no CI on any live repo. The pr-validation.yml GitHub Actions workflow existed only in the archived v1 sv-portal; sv-portal-2026 has no .github/workflows. The pre-push gate is local and manual: npm run build (which runs tsc -b first) plus npm run test. Branch protection is trust-based.

Edge Functions

cd ~/code/sv-edge
./scripts/deploy-customer-facing.sh   # Deploy all 19 customer-facing functions
./scripts/smoke-edge.sh               # Post-deploy smoke test
# OR manually:
supabase functions deploy <function-name> --no-verify-jwt

Always run ./scripts/check-deps.sh before deploying. Deploy ALL customer-facing functions together to avoid version drift.

Database Migrations

cd ~/code/sv-db
supabase db push --linked

Requires: Docker Desktop running

Git Configuration

SettingValue
Author Emailzach@mystoragevalet.com
Author NameZachary Brown

Use only real, verified email addresses. Fabricated emails cause Vercel permission failures.

Architecture Constraints (Non-Negotiable)

  1. 6 customer surfaces only: Login, Dashboard, Items + Add Item, Item Detail, Booking, Account
  2. Staff routes live in the v2 portal’s /ops surface (portal.mystoragevalet.com/ops), staff-gated (is_staff()) and AAL2-stepped-up
  3. Supabase backend only โ€” no custom API servers
  4. Stripe Hosted flows only โ€” no custom card UI
  5. Single pricing tier: $299/month
  6. Magic links only โ€” no password auth
  7. Portal is authentication-only โ€” account creation happens exclusively through the website registration flow (Stripe Checkout). The portal login does not create new users.
  8. RLS on all tables โ€” zero cross-tenant access
  9. Private storage bucket โ€” signed URLs, 1h expiry

Architecture Notes

Insurance Coverage & Capacity (one custody predicate)

Included insurance coverage ($2,000 per customer) and plan capacity (200 cu ft) are enforced server-side. Both meters derive from the single shared custody predicate fn_v2_item_in_custody — an item counts toward space and coverage when it is stored or scheduled for delivery (still in SV’s hands). The portal binds to the v_v2_insurance_coverage read-model; no limits are hardcoded in the frontend (F13, Jul 13, 2026).

Per-customer overrides & the four-reader cap contract (F13.1, Jul 15, 2026): effective caps are coalesce(customer_profile.insurance_cap_cents_override, plans.insurance_cap_cents) and coalesce(customer_profile.storage_capacity_cubic_feet, plans.cubic_feet_limit). Overrides are ops-set only — customer UPDATE on the override column is revoked. Exactly four readers resolve the coverage cap: the v_v2_insurance_coverage view, fn_v2_read_meter_preview, sv.fn_v2_meter_thresholds_check, and fn_v2_ops_booking_detail. Any future cap change must touch all four in the same migration. Crossing plans.meter_warn_threshold_pct (80%) records a meter_threshold_crossed event — an ops signal, never a customer upsell. Portal meter copy states what is and routes to a human; product mechanics and pricing are banned (guard-tested in meterStrings.test.ts).

Ops missing-dimensions counts (F1, Jul 17, 2026): the /ops booking-detail surface shows two measurement-gap counts from fn_v2_ops_booking_detail: items_missing_dims is manifest-scoped (the unmeasured items on this visit — the work-prompt staff act on at pickup) and account_items_missing_dims is the account-wide backfill count (every unmeasured item the member owns, independent of the current booking). Migration 20260717170000.

Scheduled-in space preview (F4, Jul 19, 2026): the dashboard space meter now previews inbound pickup-scheduled items from two append-only view columns (scheduled_in_cubic_feet, scheduled_in_has_unmeasured; the view is now 16 columns). The render is annotation-primary — “space is measured at pickup” in membership voice — with a hatched +N% segment only when a measured sum exists; the number is suppressed when the known sum is 0 and unmeasured items remain. Customer copy stays percent-only (never cubic feet), guard-tested in meterStrings.test.ts.

Common Issues & Solutions

IssueSolution
Webhook 401Deploy with --no-verify-jwt
CORS blocking POST (OPTIONS 200, POST never fires)Ensure Access-Control-Allow-Headers includes apikey, x-client-info alongside authorization, content-type
Edge function auth 401 despite valid JWTUse getUser(token) with explicit token arg, not getUser() on a stateless client
Migration not appliedRun supabase db push --linked
Docker not runningStart Docker Desktop
RLS blocking queryCheck user_id matches auth.uid()
Preview auth redirects failSupabase Auth requires preview domains to be allowlisted in URL Configuration (Redirect URLs). If not allowlisted, magic links may redirect to production or fail.
React Query key mismatchBookings list cache uses query key ['bookings-list']. Do not invalidate ['bookings'] expecting it to refresh the dashboard; ensure key alignment.
Mac Studio disk filling up — Photos ignoring “Optimize Mac Storage”macOS purges evictable iCloud content on demand only, never on a schedule or threshold — so it will sit on 130 GB+ of local originals indefinitely while free space looks comfortable. Force it: write a ballast file with mkfile <N>g, then request more space than remains free (e.g. mkfile 32g with 12 GB free) so macOS must purge to satisfy it; repeat, then rm every ballast file in a separate command (a trailing cleanup line in a long loop can be killed by a timeout, stranding the ballast). Verify the evictable pool first via read-only SQL on Photos.sqlite (ZINTERNALRESOURCE: ZLOCALAVAILABILITY=1 AND ZREMOTEAVAILABILITY=1 is safe to purge). Recovered 120 GB with zero data loss Jul 28, 2026. CleanMyMac does not block native purging (tested); never accept its offer to remove Photos Library.photoslibrary — that deletes the whole library, unverified, while the Photos daemons are live.

Platform Operations Toolkit

A tiered toolkit for session integrity, repository hygiene, and infrastructure observability. Introduced March 2026 after an orphaned PR went undetected for 35 days. See DEC-017 for rationale.

Commands

CommandPurposeWhen to Use
sv-audit Session integrity and repo hygiene detection. Checks git status, stale PRs (>7d), orphan branches (>30d), secrets tripwire, root directory policing, leaked code-sign clone detection (apps like Chrome that crash-loop and accumulate temp copies of themselves), 1Password socket cleanup. Non-destructive. End of every session (mandatory)
sv-hygiene Deletes remote branches already merged into main. Never deletes unmerged work. Weekly, or after audit flags stale branches
sv-health Platform observability: Supabase migration drift, Vercel deployment failures, GitHub Actions failures, Stripe webhook backlog. Weekly, or when verifying production health
sv-check Unified command: runs audit, auto-triggers hygiene if ATTENTION, runs health checks, then the Sales-Engine smoke test. The "is everything OK?" command
sales-engine-check.sh Non-destructive smoke test of the Sales-Engine across every layer: data source-of-truth + drift, local asset generation + QR decode, send-pipeline safety gates (no email sent), and live promo-page reachability. Drive desktop distribution was retired Sep 16, 2026; a missing mount must not trigger a client relaunch or publisher run. Standalone: bash ~/Documents/storagevalet/Marketing/Collateral/promo-cards-v6/sales-engine-check.sh; also runs automatically inside sv-check. After changing the Sales-Engine; runs in every sv-check

All Shell Shortcuts

Defined in ~/.zshrc. Run sv-help to see this list in the terminal.

CommandWhat It Does
sv <repo>Navigate to a repo (e.g., sv portal)
sv-statusGit status across all 7 repos (SV_REPOS)
sv-pullPull latest across all 7 repos (SV_REPOS)
sv-auditRun the end-of-session audit script
sv-hygieneWeekly repo cleanup (merged branches)
sv-healthPlatform health check (migrations, deploys, webhooks)
sv-checkFull platform check (audit + hygiene + health)
sv-remotePre-flight checks for remote Claude Code sessions (1Password unlock, secrets, Docker, 1Password biometric agent socket for git signing, GitHub CLI)
sv-issuesOpen GitHub issues across all repos
sv-envLoad secrets from 1Password (cached for sub-agents)
sv-env-clearClear cached secrets and remove session file
sv-helpShow all available commands
gt-helpShow Ghostty keyboard shortcuts and tips
title <name>Set iTerm2 tab title (no args = reset to auto directory name)
stripeAuthenticated via STRIPE_API_KEY env var (from sv-env); auto-detects live mode. For test mode: env -u STRIPE_API_KEY stripe <cmd>.

iTerm2 Tab Auto-Naming

Tabs automatically show the current directory name via a precmd hook in ~/.zshrc. Use title "My Topic" to set a custom title, or title (no args) to reset to auto. Claude Code also sets the tab title to the chat name when running.

Setup (one-time per machine): Disable custom tab titles in iTerm2’s plist (quit iTerm2 first, run via Terminal.app), then add the _update_tab_title precmd function and title() helper to ~/.zshrc. Both machines were configured March 2026.

Ghostty Config Sync

Ghostty configuration lives in iCloud at ~/Documents/storagevalet/Technology/Ghostty/config and is symlinked to ~/.config/ghostty/config on each machine.

Clickable folder paths — use file:// URLs, not Ghostty config. Agents render folder/file deliverables as bare file:// URLs (absolute path, spaces as %20) so Zach can Cmd+click straight to Finder — verified working in Ghostty 1.3.1 on Jul 15, 2026. This works because link-url = true already matches URL schemes. It must be a bare URL: a markdown link hides the URL text and the matcher never sees it. Do not attempt to configure this via Ghostty’s link option (custom regex → clickable file paths) — it is documented but stubbed out (TODO: This can’t currently be set! as of 1.3.1), so any such config silently does nothing. Re-check link on major Ghostty upgrades; a real regex matcher would supersede the convention. Enforced as rule 9 in the global CLAUDE.md.

1Password SSH Agent

The 1Password SSH agent is configured on both machines for any SSH-based operations (ssh to other hosts, SSH-backed git clones, etc.). Git commit signing is intentionally disabled — 1Password’s signing-authorization prompts appear on the Mac Studio screen and cannot be answered when working remotely via the Claude app’s code tab. Sole-developer context plus private repos means signing has no practical benefit to offset that friction.

FilePurpose
~/.zshrcExports SSH_AUTH_SOCK pointing to the 1Password agent socket
~/.ssh/configSets IdentityAgent to the same socket for all hosts
~/.config/1Password/ssh/agent.tomlTells the 1Password SSH agent which key to serve (required — auto-discovery is unreliable)

SSH_AUTH_SOCK (in ~/.zshrc):

export SSH_AUTH_SOCK="$HOME/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"

agent.toml (in ~/.config/1Password/ssh/):

[[ssh-keys]]
item = "Github SSH Key (SV)"
vault = "Storage Valet"

Key details: The SSH key is a single ed25519 key named “Github SSH Key (SV)” stored in the Storage Valet vault. Both machines share the same key via 1Password vault sync. The key is registered on GitHub for authentication on any SSH-based git remotes.

Script Locations

All scripts are canonical in ~/code/sv-docs/scripts/ (git-tracked). A convenience symlink at ~/code/sv-final-audit.sh points to the canonical audit script for manual use, but no script depends on this symlink.

ScriptCanonical Path
Session audit~/code/sv-docs/scripts/sv-final-audit.sh
Repo hygiene~/code/sv-docs/scripts/repo-hygiene.sh
Platform health~/code/sv-docs/scripts/platform-health-check.sh
Unified check~/code/sv-docs/scripts/sv-check.sh
Retired Drive backup (Sep 16, 2026)~/code/sv-docs/scripts/sv-backup.sh — historical path; do not schedule or relaunch

Backups and Desktop-Sync Retirement

Current status — founder decision, Sep 16, 2026

Google Drive and Dropbox desktop sync, the daily 03:00 Drive backup, and local Drive publishing are retired. Do not automatically relaunch either desktop client, re-enable com.storagevalet.backup, or run the retired Drive publisher during setup, maintenance, audits, or asset generation.

This is a desktop-workflow retirement. Google and Dropbox accounts, existing cloud files and sharing permissions remain unchanged. Gmail, Google Calendar, and provider connector availability are unchanged. Existing cloud-only documents, shared objects and contract workflows remain available through manual web access; desktop retirement does not mean their links are broken.

Current business source: ~/Documents/storagevalet/. Start with its START-HERE.md for working locations. Preserved local cloud exports are filed at ~/Documents/storagevalet/_Archive/2026-09-16-cloud-retirement/preserved/; the retained cloud accounts remain the access path for cloud-only objects.

Replacement backup status: a local restore ZIP was created and verified on Sep 17, 2026 at ~/Library/Application Support/Storage Valet/Backups/Storage-Valet-local-restore-20260917T072833Z.zip (5,237 regular files; 6,963,218,581 ZIP bytes). Every archive member passed CRC and SHA-256 checks, and the postcard PDFs and source files passed sample restore checks. The capture had no dataless or read gaps; regenerable caches and Git metadata were excluded, and symbolic links were preserved without following them. This is a file-data restore point, not a complete Mac image. An off-device backup replacement is not configured. iCloud is sync, not backup — deletions can propagate. A local restore point alone does not protect against losing the Mac. GitHub separately retains pushed repository history.

Historical Drive pipeline — retired Sep 16, 2026

sv-backup.sh previously zipped ~/Documents/storagevalet locally in /tmp, checked the archive, then moved it to Google Drive as My Drive/SV-Backups/SV-Full-Backup-YYYY-MM-DD.zip. The com.storagevalet.backup launchd agent ran at 03:00; its log was ~/Library/Logs/sv-backup.log. It was weekly with eight retained versions before Jul 28, then daily with 21 retained versions. These are historical settings, not a current schedule.

The job ran on the Mac Studio because it held the local Documents copy. The MacBook Air’s Optimize Storage setting could leave dataless placeholders and produce incomplete archives. A Jul 28 verification measured 1.6GB and 2,588 entries; those dated figures are not current coverage or size guarantees.

Historical lesson: the 40-day silent failure (Jun 7 – Jul 12, 2026)

The old script published six consecutive 124-byte archives containing only an empty storagevalet/ entry and logged Backup OK. Discovered Jul 15, the cause was that the launchd process did not inherit Full Disk Access from the terminal. It could read the source directory entry but not the contents. The historical fix granted access to the interpreter, /bin/zsh.

Lesson retained: zip -T proves structural validity, not useful contents. A terminal run also cannot prove that a scheduled job has permission to read its sources. Validate any replacement in its real execution context and inspect the resulting content, size, age and ability to restore.

The Jul 15 guards (c932fe5) rejected archives below 500 entries or 50MB and left the previous good snapshot untouched. Rotation also required zsh’s (N) NULL_GLOB qualifier: redirecting stderr does not prevent a shell-level unmatched-glob abort. Preserve these lessons when designing a replacement; do not restart the retired agent to test them.

Historical lesson: the 19-day silent failure (Aug 7 – Aug 26, 2026)

The last good run was Aug 7 at 03:00, after which the log stopped growing. The agent remained loaded with a registered calendar trigger but did not run; the root cause was not established. The Aug 26 repair re-registered the agent and verified that launchd could spawn it. Those recovery steps applied to the former workflow and are not instructions to re-enable it now.

Lesson retained: a backup can fail by producing unusable output or by never running, with no new error entry. Check the newest artifact’s age, contents and size. The historical [BACKUP FRESHNESS] check used a two-day age limit and 50MB floor; a skipped check when Drive was unmounted did not prove backup coverage.

Superseded decision — Drive-only backup risk accepted Sep 1, 2026

The earlier founder decision accepted Drive as the business-backup destination while the daily ZIP job was active, with GitHub for pushed code history and no Time Machine destination. The Sep 16 retirement supersedes that coverage assumption. Do not cite the Sep 1 decision as evidence of a current off-device backup or use it to hide the replacement gap. The earlier tracking reference is sv-docs#5; its status is not changed by this documentation.

Design Principles

Developer Setup

Code Repositories

~/code/
โ”œโ”€โ”€ sv-portal-2026/  # LIVE customer portal (Vite 8 / React 19 / Tailwind 4) โ€” cut over Jun 10, 2026
โ”œโ”€โ”€ sv-portal/       # v1 portal (Vite 5 / React 18 / Tailwind 3) โ€” DECOMMISSIONED + archived Jun 29, 2026
โ”œโ”€โ”€ sv-edge/         # Supabase Edge Functions (Deno/TypeScript)
โ”œโ”€โ”€ sv-db/           # Database migrations (SQL)
โ”œโ”€โ”€ sv-docs/         # Operational scripts, runbooks, and archive
โ”œโ”€โ”€ sv-website/      # Landing page (www.mystoragevalet.com)
โ””โ”€โ”€ sv-wiki/         # Internal wiki (wiki.mystoragevalet.com)

Sync Strategy

Mac Studio (primary): iCloud Desktop & Documents sync ON, all files stored locally.
MacBook Air (secondary): iCloud sync ON + "Optimize Mac Storage" ON (auto-offloads when space is tight).
Google Drive / Dropbox desktop sync: Retired Sep 16, 2026; no automatic relaunch. Cloud accounts, files, sharing and manual web access remain available. See current backup status and the retired workflow. iCloud remains sync, not backup.

File organization (Jun 2026): ~/Documents has two domains โ€” storagevalet/ (all business files) and personal/ โ€” documented on disk in ~/Documents/_README.md. Machine tooling must stay at storagevalet/Technology/{Brewfile, Shell/, Ghostty/, Raycast/}: Raycast script registration, the Ghostty config symlink, and sv-audit drift checks hard-code these paths on both machines. Relocating them breaks tooling everywhere (happened Jun 10, 2026 via an automated Documents cleanup; restored same day).

New Machine Setup

When setting up Claude Code on a new machine:

  1. Ensure iCloud Desktop & Documents sync is enabled (System Settings โ†’ Apple ID โ†’ iCloud โ†’ iCloud Drive)
  2. Create symlinks:
    ln -sf ~/Documents/storagevalet/Technology/Reference/CLAUDE.md ~/.claude/CLAUDE.md
    ln -sf ~/Documents/storagevalet/Technology/Reference/code-CLAUDE.md ~/code/CLAUDE.md
  3. Clone repos to ~/code/: sv-portal, sv-portal-2026, sv-edge, sv-db, sv-docs, sv-website, sv-wiki
  4. Install Node.js via Homebrew: brew install node@24 (do NOT use .pkg installer)
  5. Install CLI tools: brew install supabase gh stripe deno pipx
  6. Install Homebrew-managed casks: brew install --cask 1password-cli appcleaner bettermouse claude-code@latest cleanshot codex cursor font-jetbrains-mono framer gcloud-cli ghostty hazel loom miro obsidian rectangle-pro spotify textexpander typora visual-studio-code whimsical wispr-flow
    Note: claude-code cask is used on Mac Studio. On MacBook Air, Claude Code is installed via the native installer (claude install, installs to ~/.local/bin/claude). Do NOT install via npm — the npm package @anthropic-ai/claude-code is deprecated in favor of native installers.
  7. Install remaining apps directly: Docker Desktop (required for Supabase CLI), 1Password (biometric agent for op CLI), iTerm2 (custom Dock icon; has reliable built-in updater), Google Chrome — these must NOT be installed via Homebrew as replacement can break system integrations
  8. Do not install or enable Google Drive / Dropbox desktop sync or the retired 03:00 Drive backup as part of default machine setup. Use manual web access to retained cloud files until a replacement workflow is approved.
  9. Run op signin to authenticate 1Password

App Update Strategy (as of March 2026)

Desktop apps are managed in three tiers:

  1. Homebrew casks (26 packages): Updated via brew upgrade --greedy. The --greedy flag is required because most casks are marked auto_updates, which plain brew upgrade skips. Includes desktop apps, CLI tools, and fonts.
  2. App Store apps: Updated via mas upgrade (requires password in terminal).
  3. Direct-install apps (1Password, Docker, iTerm2, Google Chrome): Self-update or update via their own mechanisms. These are excluded from Homebrew to avoid breaking system-level integrations (biometric auth, Docker VM). Retired Drive / Dropbox desktop clients must not be relaunched or re-enabled by maintenance.

The Raycast script SV Update All (sv-update-all.sh) is the all-in-one daily maintenance command. It runs Homebrew (--greedy + cleanup), App Store checks, Docker cleanup (docker system prune -f, guarded), and Claude Code plugin updates. The npm section was removed in March 2026 after Claude Code migrated to native installers. This script replaces the need for the separate sv-brew shell function.

The Raycast snippet SV Platform โ€” Daily Update & Maintenance (keyword ;update, canonical JSON at Technology/Raycast/Snippets/update_raycast-snippet.json) is the Claude Code counterpart: pasted into a fresh session, it drives the full daily routine โ€” tooling updates (Homebrew, npm globals, plugins, App Store), MCP/connector health (claude mcp list, all must show Connected), platform health (sv-health), and stale-session cleanup โ€” reporting one compact table. Note for agents: the plugin-update loop must pipe through while read because zsh does not word-split unquoted variables.

A shared Brewfile at ~/Documents/storagevalet/Technology/Brewfile (iCloud-synced) is the canonical source for all Homebrew-managed packages. On a new machine, run brew bundle install --file=~/Documents/storagevalet/Technology/Brewfile then skip machine-specific packages as needed (see per-machine notes above).

Machine Topology & Machine-Aware Maintenance (single source of truth)

Why this exists: the MacBook Air is disk/RAM-constrained, so apps it doesn’t use locally get removed. The old uniform maintenance routine assumed both machines were identical, so every removal broke it. Per-machine divergence is now declared data, not implicit state.

  • Mac Studio = canonical dev environment (always-on, caffeinated, RC host). Runs the FULL daily SV maintenance protocol.
  • MacBook Air = thin-ish client; dev work happens via RC into the Studio, with light local upkeep only. Aggressive local app removal is expected and fine.
  • Selector: ~/.sv-machine (local, non-synced, studio|mba) — deliberately a local marker, not the hostname, because both machines report hostname Mac.
  • Data: ~/Documents/storagevalet/Technology/machine-profiles.md (iCloud, shared). Read both before treating any absence as a problem.
  • Intentionally-absent tools are intentional — do NOT reinstall or flag as drift. MBA: Docker (reinstall on-demand, only where a migration runs) + GitHub Desktop. Studio: GitHub Desktop (removed Jun 29, 2026; redundant with Claude Code, Codex, gh CLI, web UI). Docker IS present on the Studio (the dev/migration machine) — keep it.

The ;update routine now carries a Part 0: Machine awareness preamble: read the selector + profile first, never flag declared-absent tools, run only light upkeep on a thin client, and never let one failed formula abort the whole brew upgrade batch.

Stripe CLI = homebrew-core stripe-cli on BOTH machines. The legacy stripe/stripe-cli tap was removed Jun 29, 2026 after a completion-file collision aborted a --greedy upgrade (silently blocking every step queued behind it). The Brewfile no longer taps it — do not re-add the tap.

A “successful” brew upgrade can leave the package uninstalled. On Sept 1, 2026 brew upgrade pipx printed Upgraded 1 requested outdated package / pipx 1.16.7 -> 1.17.1 and exited 0, but afterwards /opt/homebrew/Cellar/pipx did not exist and brew info pipx reported Not installed. Because the GA4 MCP launcher ends in exec pipx run analytics-mcp, the analytics connector died instantly — the only symptom being analytics-mcp … โœ˜ Failed to connect in claude mcp list. Fix: brew install pipx, then re-run claude mcp list to confirm โœ” Connected. Same family as the stripe-tap and stuck-cask-metadata incidents: brew's own success output is not evidence the tool is on disk. When a maintenance run upgrades a formula that something else execs, verify the binary resolves (which <tool>) rather than trusting the upgrade summary.

Portability Rule

~/.claude/CLAUDE.md must never contain absolute paths starting with /Users/ or /Library/. Use ~/ and symlink resolution instead. Both CLAUDE.md files are iCloud-synced via symlinks:

  • ~/.claude/CLAUDE.md โ†’ ~/Documents/storagevalet/Technology/Reference/CLAUDE.md (global context)
  • ~/code/CLAUDE.md โ†’ ~/Documents/storagevalet/Technology/Reference/code-CLAUDE.md (cross-repo session startup)

Session Startup Checklist

Run these steps at the beginning of each Claude Code session (human or agent):

  1. Check for CLAUDE.md sync conflicts:
    CLAUDE_DIR="$(cd "$(dirname "$(readlink ~/.claude/CLAUDE.md || echo ~/.claude/CLAUDE.md)")" && pwd)"
    ls "$CLAUDE_DIR" 2>/dev/null | grep -i conflict && echo "CLAUDE.md CONFLICT โ€” resolve before continuing"
  2. Pull latest for all repos:
    for repo in sv-portal sv-edge sv-db sv-docs sv-website sv-wiki; do (cd ~/code/$repo && git pull); done
  3. Verify migrations: supabase migration list --linked
  4. Read context: ~/.claude/CLAUDE.md and ~/code/CLAUDE.md
  5. Docker Desktop must be running for Supabase CLI

Session End Protocol

  1. Run sv-audit (or sv-check for full platform verification) โ€” do not declare clean until Audit: CLEAN
  2. If the audit flags GitHub hygiene issues (stale PRs, orphan branches): review and resolve
  3. If session was significant (deploy, E2E verification, architecture decision): update the relevant SV-Wiki pages and bump each page’s Last updated: line
  4. Run python3 ~/code/sv-wiki/generate-llms.py — skipping it leaves llms.txt and llms-full.txt stale even when the HTML is correct, and those endpoints are what most agents actually read
  5. Commit the HTML plus both regenerated endpoints together and push to main (instant production; there is no CI)

See Platform Operations Toolkit for the full command reference.