Architecture Overview

Tech Stack

LayerTechnologyPurpose
FrontendReact + Vite + TypeScriptCustomer portal
BackendSupabase (PostgreSQL + Edge Functions)Database, auth, serverless
HostingVercelFrontend deployment
PaymentsStripeBilling, subscriptions
SchedulingCalendlyAppointment booking
CRMHubSpot (Free)Partnership sales pipeline
AuthSupabase Auth (Magic Links)Passwordless authentication
StorageSupabase StorageItem photos

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-portalportal.mystoragevalet.comsv-portal
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 (shell alias defaults to live mode; use command 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
WorkflowRaycast (app switching, shortcuts, clipboard history, custom scripts)

MCP Servers (Claude Code Integrations)

Claude Code connects to 17 MCP servers, organized by type. These give Claude direct, tool-based access to external services without manual API calls or dashboard navigation.

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)
Calendlymcp__calendly__*List/cancel events, view invitees, manage schedulingPAT from 1Password via launcher script
Google Workspacemcp__google-workspace__*Google Docs, Sheets, and Drive — read, write, format, searchOAuth tokens in ~/.config/mcp-google-workspace/
Resendmcp__resend__*Send/manage emails, contacts, domains, broadcasts — debug delivery issuesAPI key from 1Password via launcher script
GA4 Analyticsmcp__analytics-mcp__*Query website traffic, run reports, realtime visitors — read-onlyApplication Default Credentials via gcloud

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

ConnectorTool PrefixPurpose
Canvamcp__claude_ai_Canva__*Design creation and management
Excalidrawmcp__claude_ai_Excalidraw__*Diagramming and whiteboarding
Gmailmcp__claude_ai_Gmail__*Read/draft/search email
Google Calendarmcp__claude_ai_Google_Calendar__*Event management, free time, scheduling
HubSpotmcp__claude_ai_HubSpot__*CRM pipeline management (sales only)
Jotformmcp__claude_ai_Jotform__*Form creation and submission management
Stripemcp__claude_ai_Stripe__*Customers, subscriptions, invoices, products
Vercelmcp__claude_ai_Vercel__*Deployments, projects, build logs
Zapiermcp__claude_ai_Zapier__*Automation workflows and integrations

Built-in MCPs (bundled with Claude Code)

ServerTool PrefixPurpose
Claude in Chromemcp__claude-in-chrome__*Browser automation, screenshots, form filling, page reading
Context7mcp__plugin_context7_context7__*Live API documentation lookup for any library
Supabase (plugin)mcp__plugin_supabase_supabase__*Backup/fallback Supabase connection

MCP Launcher Script Pattern

Three MCP servers use launcher scripts to inject secrets from 1Password at runtime, avoiding plaintext credentials on disk:

# Pattern: ~/Documents/storagevalet/Technology/Utility_Scripts/{name}-mcp-launcher.sh
# Each script fetches its secret via op read, then exec's the MCP server process.

# Calendly: calendly-mcp-launcher.sh โ†’ fetches PAT โ†’ npx calendly-mcp-server
# Resend:   resend-mcp-launcher.sh   โ†’ fetches API key โ†’ npx resend-mcp
# GA4:      ga4-mcp-launcher.sh      โ†’ sets ADC path โ†’ pipx run analytics-mcp

GA4 MCP Infrastructure

The GA4 Analytics MCP requires a Google Cloud project for OAuth credentials:

ItemValue
Google Cloud Projectstorage-valet-analytics
OAuth ClientSV Analytics MCP (Desktop app type)
Client JSON~/.config/gcloud/ga4-oauth-client.json
ADC Token~/.config/gcloud/application_default_credentials.json
APIs EnabledAnalytics Admin API, Analytics Data API
1Password ItemGoogle Cloud - GA4 Analytics MCP
Scopesanalytics.readonly, cloud-platform (read-only)

If the ADC token expires, re-run: CLOUDSDK_PYTHON=/opt/homebrew/bin/python3.13 gcloud auth application-default login --scopes=https://www.googleapis.com/auth/analytics.readonly,https://www.googleapis.com/auth/cloud-platform --client-id-file=~/.config/gcloud/ga4-oauth-client.json

Repository Structure

Integrated System (4 repos, tightly coupled)

These repos share a common Supabase backend and must be considered together:

~/code/sv-portal     # Customer-facing React app
~/code/sv-edge       # Supabase Edge Functions (Deno/TypeScript)
~/code/sv-db         # Database migrations (SQL)
~/code/sv-docs       # System documentation

Standalone Projects

RepoPurposeURL
sv-websiteMarketing site, signup flowwww.mystoragevalet.com
sv-wikiInternal docs (AI + human reference)wiki.mystoragevalet.com

These repos have NO Supabase integration, NO shared code with the portal.

sv-portal (Customer Portal)

Overview

Customer Routes (Non-Negotiable)

RoutePurpose
/loginMagic link authentication
/dashboardItem inventory + booking management
/scheduleService scheduling flow
/accountUser settings

Staff/Admin Routes (Not Customer-Facing)

RoutePurpose
/opsOperations dashboard (staff-gated)
/admin/waitlistWaitlist management (staff-gated)
/admin/customersCustomer management (staff-gated)

Key Components

src/
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ ProtectedRoute.tsx  # Auth guard (all protected routes)
โ”‚   โ”œโ”€โ”€ ItemCard.tsx        # Individual item display
โ”‚   โ”œโ”€โ”€ BookingsList.tsx    # Booking management
โ”‚   โ”œโ”€โ”€ AddItemModal.tsx    # Item creation
โ”‚   โ””โ”€โ”€ EditItemModal.tsx   # Item editing
โ”œโ”€โ”€ pages/
โ”‚   โ”œโ”€โ”€ Login.tsx           # Auth page
โ”‚   โ”œโ”€โ”€ Dashboard.tsx       # Main inventory view
โ”‚   โ”œโ”€โ”€ Schedule.tsx        # Booking flow
โ”‚   โ””โ”€โ”€ Account.tsx         # User settings
โ””โ”€โ”€ lib/
    โ””โ”€โ”€ supabase.ts         # Supabase client

Commands

cd ~/code/sv-portal
npm run dev          # Start development server (localhost:5173)
npm run build        # Build for production (tsc + vite build)
npm run typecheck    # TypeScript validation (canonical command)
npm run lint         # Alias for typecheck (until ESLint is added)
vercel               # Preview deploy (Zach promotes to production)

sv-edge (Edge Functions)

Overview

Functions (16 deployed)

Stripe Integration

FunctionPurposeTrigger
create-checkout-trialStripe checkout with 14-day trial (LIVE)Landing page
create-checkoutStripe checkout (legacy, no trial)Deprecated
create-portal-sessionStripe billing portalPortal
stripe-webhookProcess Stripe eventsWebhook

Calendly Integration

FunctionPurposeTrigger
calendly-webhookCreate bookings from Calendly eventsWebhook

Booking Operations

FunctionPurposeTrigger
bookings-listList all bookings (ops dashboard)Ops UI
booking-getGet single booking with itemsOps UI
booking-cancelCancel booking, revert item statusesPortal, Ops UI
complete-serviceMark service completedOps UI
update-booking-itemsModify items on pending bookingPortal

Customer & Email

FunctionPurposeTrigger
admin-create-customerCreate customer bypassing StripeOps/admin
signup-webhookPre-register, check service areaLanding page
send-emailTransactional emails via ResendSystem

Data Export

FunctionPurposeTrigger
full-exportExport all customer dataAdmin (deployed directly, not in repo)

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 use the deploy script (recommended)
./deploy-and-test.sh

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

70 property-specific promo pages are served from a single HTML file at sv-website/promo/index.html. No build step, no framework, no external CSS dependencies. See Partnerships > Promo Landing Pages for the business context.

How Routing Works

A Vercel rewrite rule in sv-website/vercel.json maps all promo slugs to a single file:

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

Vercel's cleanUrls: true setting automatically serves promo/index.html at the /promo path. When the page loads, client-side JavaScript:

  1. Reads the URL slug (e.g., beacon from /promo/beacon)
  2. Looks it up in a PROPERTIES object embedded in the HTML (70 entries)
  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.htmlThe promo page (single file, all 70 properties — inline CSS + JS)
sv-website/vercel.jsonRewrite rule: /promo/:code/promo
sv-docs/specs/promo-landing-pages.mdTechnical implementation spec (future phases)
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 Charcoal Blue (#213C47) for brand consistency. 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. PM-originated share links include ?src=pm for analytics attribution. The toggle is implemented client-side — the .share-section element is hidden by default and displayed only when isPM is true.

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. Commit and push to main. Vercel auto-deploys within ~60 seconds.
  4. 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. Deactivate the corresponding Stripe promotion code
  3. Commit and push — the URL will then show the “Page Not Found” fallback

Planned Enhancements

Future phases (documented in sv-docs/specs/promo-landing-pages.md) will add:

Integrations

Stripe

ItemValue
ModeLIVE
Product$299/month subscription
Trial14-day complimentary on all new subscriptions
Setup FeeNone (eliminated)
Webhook Versionv4.1
Events Processedcheckout.session.completed, customer.subscription.updated, invoice.payment_succeeded, invoice.payment_failed

Calendly

ItemValue
Eventsinvitee.created, invitee.canceled
IntegrationWebhook only (no outbound API calls)
Signal vs TruthCalendly is signal; database is canonical

Supabase Storage

ItemValue
Bucketitem-photos (private)
AccessSigned URLs (1-hour expiry)
Max Size20MB per file
Allowed Typesimage/jpeg, image/png, image/webp
Max Photos5 per item

Vercel Web Analytics

ItemValue
PlanWeb Analytics Plus ($10/month add-on)
PagesAll 8 sv-website pages (index, partnerships, faq, privacy, terms, testimonials, signup/success, signup/canceled)
IntegrationScript tags: /_vercel/insights/script.js (Speed Insights) + /_vercel/analytics/script.js (Web Analytics)
Reporting Window24 months (Plus tier)

Custom Events Tracked

Event NamePageData
signup_submitLanding-
signup_checkoutLanding-
signup_waitlistLandingcity, zip
cta_clickBothtext, location
faq_openBothquestion
scroll_depthBothdepth (25%, 50%, 75%, 100%)
partnership_form_submitPartnershipsproperty
partnership_form_successPartnershipsproperty
partnership_form_errorPartnershipsproperty, error
promo_page_viewPromoslug, property, code
promo_share_copyPromoslug
promo_link_copyPromoslug

Script-Tag API Pattern (Important)

Do NOT use window.va.track()

The .track() method only exists in the npm package (@vercel/analytics). The script-tag integration sets window.va as a function, not an object. Using .track() throws TypeError: window.va?.track is not a function.

// CORRECT โ€” script-tag API
window.va?.('event', { name: 'signup_submit' });
window.va?.('event', { name: 'cta_click', data: { text: 'Get Started', location: 'hero' } });

// WRONG โ€” npm package API (will throw TypeError)
window.va?.track('signup_submit');
window.va?.track('cta_click', { text: 'Get Started' });

Google Analytics (GA4)

Added Feb 28, 2026. Provides deeper behavioral analytics beyond what Vercel Web Analytics captures โ€” user flows, engagement time, acquisition channels, and conversion events.

ItemValue
AccountMy Storage Valet LLC
Propertymystoragevalet.com
Measurement IDG-9SBYJ8QEC1
Stream ID13680726133
Stream NameStorage Valet Website
CostFree (included with Google account)
PagesAll 8 sv-website pages (same as Vercel Analytics)
Integrationgtag.js snippet placed before Vercel scripts in each HTML file
Dashboardanalytics.google.com (sign in with zach@mystoragevalet.com)
TimezoneEastern (US)
IndustryBusiness & Industrial
Data Collection StartFeb 28, 2026 (no retroactive data โ€” only captures from snippet deployment onward)

Enhanced Measurement (Auto-Tracked)

These events are captured automatically by GA4 with no additional code:

EventWhat It Captures
Page viewsEvery page load and client-side navigation
ScrollsWhen user scrolls past 90% of page
Outbound clicksClicks to external domains (e.g., portal.mystoragevalet.com, Stripe checkout)
Site searchQuery parameters in URL (not currently used)
File downloadsClicks on document/file links

Vercel Analytics vs GA4 โ€” When to Use Which

QuestionUse
How many visitors this week?Either (Vercel is simpler)
Which pages get the most views?Either
Where is traffic coming from? (referrers, UTM campaigns)GA4 (more detail)
How long do visitors spend on the page?GA4
What % of visitors scroll to the signup form?GA4 (scroll events)
Are partnership emails driving traffic?GA4 (UTM breakdown)
Custom event tracking (signup_submit, cta_click)?Vercel Analytics (already wired)
Core Web Vitals / page speed?Vercel Speed Insights

Still To Build Out

HubSpot CRM

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

ItemValue
Account ID245438948
PlanFree (Sales Hub)
Pipeline“Property Partnerships” (internal name: default)
Records225 deals (prospective partners), 20 companies, 6 contacts
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

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 Contrast--text-muted darkened from #88989A (pre-v2.0 Cool Steel, ~3.2:1) to #5F6E72 (~4.7:1) for WCAG AA compliance against parchment and white backgrounds. v2.0 uses Slate Grey (#6A7F83) for secondary text.
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 text/link color overridden from #6A7F83 (2.76:1) to #9BB0B5 (5.14:1 on #213C47 background). Passes WCAG AA. Portal feature highlight text on partnerships page also adjusted to #566A6E (5.70:1).
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); Resend MCP (via resend-mcp-launcher.sh, 1Password-backed)
GA4 OAuth Client IDGoogle Cloud - GA4 Analytics MCPclient_idGA4 MCP (via ga4-mcp-launcher.sh; ADC at ~/.config/gcloud/application_default_credentials.json)
Calendly PATCalendlyPATCalendly MCP (via calendly-mcp-launcher.sh, 1Password-backed); manual API calls
Calendly Webhook Signing KeyCalendlywebhook_signing_keySupabase secrets (CALENDLY_WEBHOOK_SIGNING_KEY)
Notion API KeyNotion APIapi_keyNotion MCP plugin (currently disabled)
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 auto-deletes on shell exit; 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 16 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 6 repositories.

Deployment

Portal (Vercel)

cd ~/code/sv-portal
npm run build        # Verify build passes
vercel               # Preview deploy (NOT --prod)

Auto-deploys from GitHub main branch. Use preview deploys for testing; Zach promotes to production manually. Do not run vercel --prod directly.

PR validation: GitHub Actions workflow (.github/workflows/pr-validation.yml) runs npm run typecheck and npm run build on all PRs to main. Both must pass before merge.

Edge Functions

cd ~/code/sv-edge
./scripts/deploy-customer-facing.sh   # Deploy all 14 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. 4 customer routes only: /login, /dashboard, /schedule, /account
  2. Staff routes permitted under /ops and /admin/* but must remain staff-gated (sv.is_staff())
  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

Included insurance coverage ($2,000 per customer) is enforced server-side via the v_user_insurance database view. All portal UI elements (coverage progress bar, remaining coverage text) derive their values from this view; no insurance limits are hardcoded in the frontend.

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.

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, 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, then runs health checks. The "is everything OK?" command

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 6 repos
sv-pullPull latest across all 6 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-issuesOpen GitHub issues across all repos
sv-brewUpdate Homebrew packages
sv-envLoad secrets from 1Password (cached for sub-agents)
sv-env-clearClear cached secrets and remove session file
sv-helpShow all available commands
title <name>Set iTerm2 tab title (no args = reset to auto directory name)
stripeAliased to stripe --live (use command stripe or \stripe for test mode)

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.

1Password SSH Agent

Both machines use 1Password’s SSH agent for Git authentication and commit signing. Three configuration files work together:

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/allowed_signersMaps email to public key for Git commit signature verification

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 both authentication and commit signing.

Git signing config (set via git config --global on both machines):

gpg.format = ssh
commit.gpgsign = true
tag.gpgsign = true
gpg.ssh.allowedSignersFile = ~/.ssh/allowed_signers

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

Design Principles

Developer Setup

Code Repositories

~/code/
โ”œโ”€โ”€ sv-portal/   # Customer-facing React app (Vite + React + TypeScript + Tailwind)
โ”œโ”€โ”€ 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).
Dropbox: Redundant โ€” subscription lapses ~Sept 2026. Not used for active files.

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-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 cleanshot codex cursor font-jetbrains-mono font-source-code-pro framer gcloud-cli github hazel loom miro notion obsidian rectangle-pro spotify superwhisper 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, Google Drive — these must NOT be installed via Homebrew as replacement can break system integrations
  8. Run op signin to authenticate 1Password

App Update Strategy (as of March 2026)

Desktop apps are managed in three tiers:

  1. Homebrew casks (25 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, Google Drive): Self-update or update via their own mechanisms. These are excluded from Homebrew to avoid breaking system-level integrations (biometric auth, Docker VM, Drive sync daemon).

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.

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).

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 relevant SV-Wiki pages and commit to sv-wiki repo

See Platform Operations Toolkit for the full command reference.