Technology
Architecture, repositories, deployment, and integrations
Last updated: Mar 17, 2026
On This Page
Architecture Overview
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React + Vite + TypeScript | Customer portal |
| Backend | Supabase (PostgreSQL + Edge Functions) | Database, auth, serverless |
| Hosting | Vercel | Frontend deployment |
| Payments | Stripe | Billing, subscriptions |
| Scheduling | Calendly | Appointment booking |
| CRM | HubSpot (Free) | Partnership sales pipeline |
| Auth | Supabase Auth (Magic Links) | Passwordless authentication |
| Storage | Supabase Storage | Item photos |
Infrastructure
| Service | Details |
|---|---|
| Supabase Project | gmjucacmbrumncfnnhua |
| Supabase URL | https://gmjucacmbrumncfnnhua.supabase.co |
| Region | us-east-1 |
| Database | PostgreSQL 17.6.1 |
| Vercel Team | StorageValet |
Vercel Projects
| Project | URL | Repo |
|---|---|---|
| sv-website | www.mystoragevalet.com | sv-website |
| sv-portal | portal.mystoragevalet.com | sv-portal |
| sv-wiki | wiki.mystoragevalet.com | sv-wiki |
Development Tools
| Category | Tools |
|---|---|
| Runtime | Node.js 24.14.0 LTS (Homebrew node@24, native ARM64) — npm 11.11.0 bundled |
| AI Development | Claude Code (native installer — Homebrew cask on Mac Studio, ~/.local/bin on MacBook Air) + plugins |
| Database | Supabase CLI, Supabase MCP |
| Hosting | Vercel CLI |
| Payments | Stripe CLI (shell alias defaults to live mode; use command stripe for test mode) |
| Analytics | gcloud CLI (Homebrew cask gcloud-cli) — Application Default Credentials for GA4 MCP |
| Python | pipx (Homebrew formula pipx) — isolated Python app runner for GA4 MCP server |
| Secrets | 1Password CLI |
| Workflow | Raycast (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)
| Server | Tool Prefix | Purpose | Auth Method |
|---|---|---|---|
| Supabase | mcp__supabase__* | Direct database access, schema exploration, SQL execution | OAuth (remote HTTP) |
| Calendly | mcp__calendly__* | List/cancel events, view invitees, manage scheduling | PAT from 1Password via launcher script |
| Google Workspace | mcp__google-workspace__* | Google Docs, Sheets, and Drive — read, write, format, search | OAuth tokens in ~/.config/mcp-google-workspace/ |
| Resend | mcp__resend__* | Send/manage emails, contacts, domains, broadcasts — debug delivery issues | API key from 1Password via launcher script |
| GA4 Analytics | mcp__analytics-mcp__* | Query website traffic, run reports, realtime visitors — read-only | Application Default Credentials via gcloud |
Cloud Connectors (managed by Anthropic, available in Claude Code + Desktop + Web)
| Connector | Tool Prefix | Purpose |
|---|---|---|
| Canva | mcp__claude_ai_Canva__* | Design creation and management |
| Excalidraw | mcp__claude_ai_Excalidraw__* | Diagramming and whiteboarding |
| Gmail | mcp__claude_ai_Gmail__* | Read/draft/search email |
| Google Calendar | mcp__claude_ai_Google_Calendar__* | Event management, free time, scheduling |
| HubSpot | mcp__claude_ai_HubSpot__* | CRM pipeline management (sales only) |
| Jotform | mcp__claude_ai_Jotform__* | Form creation and submission management |
| Stripe | mcp__claude_ai_Stripe__* | Customers, subscriptions, invoices, products |
| Vercel | mcp__claude_ai_Vercel__* | Deployments, projects, build logs |
| Zapier | mcp__claude_ai_Zapier__* | Automation workflows and integrations |
Built-in MCPs (bundled with Claude Code)
| Server | Tool Prefix | Purpose |
|---|---|---|
| Claude in Chrome | mcp__claude-in-chrome__* | Browser automation, screenshots, form filling, page reading |
| Context7 | mcp__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:
| Item | Value |
|---|---|
| Google Cloud Project | storage-valet-analytics |
| OAuth Client | SV Analytics MCP (Desktop app type) |
| Client JSON | ~/.config/gcloud/ga4-oauth-client.json |
| ADC Token | ~/.config/gcloud/application_default_credentials.json |
| APIs Enabled | Analytics Admin API, Analytics Data API |
| 1Password Item | Google Cloud - GA4 Analytics MCP |
| Scopes | analytics.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
| Repo | Purpose | URL |
|---|---|---|
sv-website | Marketing site, signup flow | www.mystoragevalet.com |
sv-wiki | Internal docs (AI + human reference) | wiki.mystoragevalet.com |
These repos have NO Supabase integration, NO shared code with the portal.
sv-portal (Customer Portal)
Overview
- Tech: Vite + React + TypeScript + Tailwind CSS
- Auth: Supabase magic links (email-based, no passwords)
- State: React Query for server state
- Customer routing: React Router (4 routes only)
Customer Routes (Non-Negotiable)
| Route | Purpose |
|---|---|
/login | Magic link authentication |
/dashboard | Item inventory + booking management |
/schedule | Service scheduling flow |
/account | User settings |
Staff/Admin Routes (Not Customer-Facing)
| Route | Purpose |
|---|---|
/ops | Operations dashboard (staff-gated) |
/admin/waitlist | Waitlist management (staff-gated) |
/admin/customers | Customer 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
- Runtime: Deno (Supabase Edge Functions)
- Auth: JWT verification + RLS
- Deployment:
supabase functions deploy
Functions (16 deployed)
Stripe Integration
| Function | Purpose | Trigger |
|---|---|---|
create-checkout-trial | Stripe checkout with 14-day trial (LIVE) | Landing page |
create-checkout | Stripe checkout (legacy, no trial) | Deprecated |
create-portal-session | Stripe billing portal | Portal |
stripe-webhook | Process Stripe events | Webhook |
Calendly Integration
| Function | Purpose | Trigger |
|---|---|---|
calendly-webhook | Create bookings from Calendly events | Webhook |
Booking Operations
| Function | Purpose | Trigger |
|---|---|---|
bookings-list | List all bookings (ops dashboard) | Ops UI |
booking-get | Get single booking with items | Ops UI |
booking-cancel | Cancel booking, revert item statuses | Portal, Ops UI |
complete-service | Mark service completed | Ops UI |
update-booking-items | Modify items on pending booking | Portal |
Customer & Email
| Function | Purpose | Trigger |
|---|---|---|
admin-create-customer | Create customer bypassing Stripe | Ops/admin |
signup-webhook | Pre-register, check service area | Landing page |
send-email | Transactional emails via Resend | System |
Data Export
| Function | Purpose | Trigger |
|---|---|---|
full-export | Export all customer data | Admin (deployed directly, not in repo) |
Utility
| Function | Purpose | Trigger |
|---|---|---|
health-check | Returns 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
| Webhook | URL |
|---|---|
| Stripe | https://gmjucacmbrumncfnnhua.supabase.co/functions/v1/stripe-webhook |
| Calendly | https://gmjucacmbrumncfnnhua.supabase.co/functions/v1/calendly-webhook |
Incident Log
| Date | Incident | Root Cause | Resolution |
|---|---|---|---|
| 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:
- Reads the URL slug (e.g.,
beaconfrom/promo/beacon) - Looks it up in a
PROPERTIESobject embedded in the HTML (70 entries) - If found: populates the page with the property name, promo code, share links, and QR code
- If not found: hides the promo content and shows a “Page Not Found” message with a link back to the main site
File Structure
| File | Purpose |
|---|---|
sv-website/promo/index.html | The promo page (single file, all 70 properties — inline CSS + JS) |
sv-website/vercel.json | Rewrite rule: /promo/:code → /promo |
sv-docs/specs/promo-landing-pages.md | Technical implementation spec (future phases) |
sv-docs/guides/promo-landing-pages.md | Comprehensive operational guide |
Design Characteristics
- Brand-consistent: Uses the same color palette (Charcoal Blue, Berry, Parchment, Stormy Teal), fonts (DM Serif Display + Inter), and design language as the main website and partnerships page
- Mobile-first responsive: Tested at 390px (iPhone), 600px (tablet), and desktop widths. Two-column layouts collapse to single-column on mobile.
- Performance: All styles inline (no external CSS). GPU-accelerated scroll animations (
translate3d,will-changelifecycle management). SVG noise texture instead of heavy images. - Accessibility:
prefers-reduced-motionsupport,focus-visibleoutlines, semantic HTML, sufficient color contrast - Animation:
translate3d(0, 28px, 0),0.8s ease-out, IntersectionObserver withthreshold: 0.1androotMargin: '0px 0px -40px 0px'— matches main site patterns
Analytics Events
Promo pages fire events to both Vercel Web Analytics and GA4:
| Event Name | Trigger | Data |
|---|---|---|
promo_page_view | Page loads with a valid slug | slug, property, code |
promo_share_copy | User clicks “Copy to clipboard” on the share snippet | slug |
promo_link_copy | User clicks the copy icon on the direct link | slug |
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:
| View | URL | Behavior |
|---|---|---|
| Resident (default) | /promo/beacon | Clean page with property name, promo code, and signup CTA. Share tools are hidden. |
| Property Manager | /promo/beacon?pm=true | Adds “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
- Lowercase only, no spaces, no hyphens, no special characters
- Must be unique across all entries in the
PROPERTIESobject - Should be recognizable (e.g.,
beacon,greyson,sable)
Adding a New Property (Technical Steps)
- 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" - Add to the PROPERTIES object in
sv-website/promo/index.html(alphabetical by slug):"newproperty": { name: "New Property Name", code: "NEWPROPERTY" }, - Commit and push to main. Vercel auto-deploys within ~60 seconds.
- Verify: Visit
mystoragevalet.com/promo/newpropertyand confirm property name, promo code, QR code, and share tools render correctly.
Removing a Property
- Remove the entry from the
PROPERTIESobject inpromo/index.html - Deactivate the corresponding Stripe promotion code
- 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:
- Embedded signup form with address fields pre-filled from property data
- Promo code auto-application at Stripe Checkout (no manual entry)
- Database attribution (
promo_codecolumn onpre_customerstable) - Additional analytics events:
promo_signup_submit,promo_checkout_redirect,promo_signup_waitlist
Integrations
Stripe
| Item | Value |
|---|---|
| Mode | LIVE |
| Product | $299/month subscription |
| Trial | 14-day complimentary on all new subscriptions |
| Setup Fee | None (eliminated) |
| Webhook Version | v4.1 |
| Events Processed | checkout.session.completed, customer.subscription.updated, invoice.payment_succeeded, invoice.payment_failed |
Calendly
| Item | Value |
|---|---|
| Events | invitee.created, invitee.canceled |
| Integration | Webhook only (no outbound API calls) |
| Signal vs Truth | Calendly is signal; database is canonical |
Supabase Storage
| Item | Value |
|---|---|
| Bucket | item-photos (private) |
| Access | Signed URLs (1-hour expiry) |
| Max Size | 20MB per file |
| Allowed Types | image/jpeg, image/png, image/webp |
| Max Photos | 5 per item |
Vercel Web Analytics
| Item | Value |
|---|---|
| Plan | Web Analytics Plus ($10/month add-on) |
| Pages | All 8 sv-website pages (index, partnerships, faq, privacy, terms, testimonials, signup/success, signup/canceled) |
| Integration | Script tags: /_vercel/insights/script.js (Speed Insights) + /_vercel/analytics/script.js (Web Analytics) |
| Reporting Window | 24 months (Plus tier) |
Custom Events Tracked
| Event Name | Page | Data |
|---|---|---|
signup_submit | Landing | - |
signup_checkout | Landing | - |
signup_waitlist | Landing | city, zip |
cta_click | Both | text, location |
faq_open | Both | question |
scroll_depth | Both | depth (25%, 50%, 75%, 100%) |
partnership_form_submit | Partnerships | property |
partnership_form_success | Partnerships | property |
partnership_form_error | Partnerships | property, error |
promo_page_view | Promo | slug, property, code |
promo_share_copy | Promo | slug |
promo_link_copy | Promo | slug |
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.
| Item | Value |
|---|---|
| Account | My Storage Valet LLC |
| Property | mystoragevalet.com |
| Measurement ID | G-9SBYJ8QEC1 |
| Stream ID | 13680726133 |
| Stream Name | Storage Valet Website |
| Cost | Free (included with Google account) |
| Pages | All 8 sv-website pages (same as Vercel Analytics) |
| Integration | gtag.js snippet placed before Vercel scripts in each HTML file |
| Dashboard | analytics.google.com (sign in with zach@mystoragevalet.com) |
| Timezone | Eastern (US) |
| Industry | Business & Industrial |
| Data Collection Start | Feb 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:
| Event | What It Captures |
|---|---|
| Page views | Every page load and client-side navigation |
| Scrolls | When user scrolls past 90% of page |
| Outbound clicks | Clicks to external domains (e.g., portal.mystoragevalet.com, Stripe checkout) |
| Site search | Query parameters in URL (not currently used) |
| File downloads | Clicks on document/file links |
Vercel Analytics vs GA4 โ When to Use Which
| Question | Use |
|---|---|
| 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
- GA4 conversion events: Mark key actions (e.g., signup form submission, Stripe checkout redirect) as conversions in GA4 for funnel tracking
- Google Ads linkage: When Google Ads is set up, link to this GA4 property for ad performance tracking
- Google Business Profile: Not yet created โ needed for local search visibility, Google Maps listing, and review collection
- Meta (Facebook) Pixel: Not yet installed โ needed if running Facebook/Instagram ads
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).
| Item | Value |
|---|---|
| Account ID | 245438948 |
| Plan | Free (Sales Hub) |
| Pipeline | “Property Partnerships” (internal name: default) |
| Records | 225 deals (prospective partners), 20 companies, 6 contacts |
| Chrome Extension | HubSpot Sales for Gmail (installed Mar 8, 2026) |
| Email Integration | Gmail — emails sent from Gmail, tracked/logged by HubSpot |
| Claude Code Access | HubSpot MCP tools (mcp__claude_ai_HubSpot__*) |
Pipeline Stages
| Stage | Internal ID | Probability |
|---|---|---|
| Research | 3313102548 | 10% |
| Outreach Sent | 3313102549 | 20% |
| Follow-up | 3313102550 | 30% |
| Meeting Scheduled | 3313102551 | 50% |
| Negotiation | 3313102552 | 75% |
| Signed Partnership | closedwon | 100% |
| Not Interested | closedlost | 0% |
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
- HubSpot is sales pipeline ONLY — tracks B2B partnership outreach to property management companies
- Not connected to product infrastructure — Stripe handles billing, Supabase handles customer data, Calendly handles scheduling
- Promo tracking: 69 deals have
promo_code+promo_urllinking to property-specific promo landing pages - Company associations: 81 deals linked to 18 management companies; 144 deals pending association
SEO & Performance (Feb 2026)
| Item | Detail |
|---|---|
| JSON-LD Structured Data | Organization 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-Content | Visually-hidden skip link on all 6 main pages (WCAG 2.4.1 Level A). |
| robots.txt | Permits all crawlers. Points to /sitemap.xml. |
| sitemap.xml | Lists all 8 HTML pages with <priority> and <lastmod> dates. Auto-deployed via Vercel. |
| Heading Hierarchy | Footer 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 Landmark | All 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 Contrast | Footer 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 Scores | Accessibility 100/100 on all pages (up from 93 landing, 91 partnerships). Best Practices 100/100 after adding favicon.ico. |
Security
Authentication
- Method: Magic links (email-based, no passwords)
- Provider: Supabase Auth
- Token: JWT with user ID
- Session: Persisted to localStorage; survives new tabs, refresh, and browser restart. Auth guard (
ProtectedRoute) gates on SupabaseINITIAL_SESSIONevent before rendering redirects.
Row Level Security (RLS)
All customer tables are protected by RLS:
- Users can only see/modify their own data
auth.uid()used in all policies- Service role bypasses RLS (edge functions only)
Billing Protection
Billing fields on customer_profile are protected:
- Users CANNOT update
subscription_status,stripe_customer_id,subscription_id - Updates only via SECURITY DEFINER functions from webhooks
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
| Secret | 1Password Item | Field | Consumers |
|---|---|---|---|
| Supabase DB Password | Supabase Production | password | Local psql only |
| Stripe Live Secret Key | Stripe Live | secret_key | Supabase secrets (STRIPE_SECRET_KEY), local Stripe CLI |
| Stripe Webhook Secret | Stripe Live | webhook_secret | Supabase secrets (STRIPE_WEBHOOK_SECRET) |
| Resend API Key | Resend API | credential | Supabase secrets (RESEND_API_KEY); Resend MCP (via resend-mcp-launcher.sh, 1Password-backed) |
| GA4 OAuth Client ID | Google Cloud - GA4 Analytics MCP | client_id | GA4 MCP (via ga4-mcp-launcher.sh; ADC at ~/.config/gcloud/application_default_credentials.json) |
| Calendly PAT | Calendly | PAT | Calendly MCP (via calendly-mcp-launcher.sh, 1Password-backed); manual API calls |
| Calendly Webhook Signing Key | Calendly | webhook_signing_key | Supabase secrets (CALENDLY_WEBHOOK_SIGNING_KEY) |
| Notion API Key | Notion API | api_key | Notion MCP plugin (currently disabled) |
| Supabase Service Role Key | Supabase Production | service_role_key | Supabase-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
- Generate/rotate the credential in the service's dashboard
- Update the value in 1Password (Storage Valet vault)
- If the secret is in Supabase secrets, push via:
op read '...' | xargs -I {} supabase secrets set KEY="{}" - 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
| Setting | Value |
|---|---|
| Author Email | zach@mystoragevalet.com |
| Author Name | Zachary Brown |
Use only real, verified email addresses. Fabricated emails cause Vercel permission failures.
Architecture Constraints (Non-Negotiable)
- 4 customer routes only:
/login,/dashboard,/schedule,/account - Staff routes permitted under
/opsand/admin/*but must remain staff-gated (sv.is_staff()) - Supabase backend only โ no custom API servers
- Stripe Hosted flows only โ no custom card UI
- Single pricing tier: $299/month
- Magic links only โ no password auth
- Portal is authentication-only โ account creation happens exclusively through the website registration flow (Stripe Checkout). The portal login does not create new users.
- RLS on all tables โ zero cross-tenant access
- 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
| Issue | Solution |
|---|---|
| Webhook 401 | Deploy 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 JWT | Use getUser(token) with explicit token arg, not getUser() on a stateless client |
| Migration not applied | Run supabase db push --linked |
| Docker not running | Start Docker Desktop |
| RLS blocking query | Check user_id matches auth.uid() |
| Preview auth redirects fail | Supabase 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 mismatch | Bookings 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
| Command | Purpose | When 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.
| Command | What It Does |
|---|---|
sv <repo> | Navigate to a repo (e.g., sv portal) |
sv-status | Git status across all 6 repos |
sv-pull | Pull latest across all 6 repos |
sv-audit | Run the end-of-session audit script |
sv-hygiene | Weekly repo cleanup (merged branches) |
sv-health | Platform health check (migrations, deploys, webhooks) |
sv-check | Full platform check (audit + hygiene + health) |
sv-issues | Open GitHub issues across all repos |
sv-brew | Update Homebrew packages |
sv-env | Load secrets from 1Password (cached for sub-agents) |
sv-env-clear | Clear cached secrets and remove session file |
sv-help | Show all available commands |
title <name> | Set iTerm2 tab title (no args = reset to auto directory name) |
stripe | Aliased 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:
| File | Purpose |
|---|---|
~/.zshrc | Exports SSH_AUTH_SOCK pointing to the 1Password agent socket |
~/.ssh/config | Sets IdentityAgent to the same socket for all hosts |
~/.config/1Password/ssh/agent.toml | Tells the 1Password SSH agent which key to serve (required — auto-discovery is unreliable) |
~/.ssh/allowed_signers | Maps 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.
| Script | Canonical 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
- Non-destructive audits:
sv-auditreads only (except 1Password socket cleanup). It never modifies git state, GitHub, or infrastructure. - Explicit cleanup:
sv-hygieneis a separate, opt-in command. It only deletes branches confirmed merged into main. - Canonical paths: Scripts reference
~/code/sv-docs/scripts/directly. Symlinks exist for convenience but are not dependencies. - Dual observability: Stale PRs are detected at session start (bootstrap snippet) and session end (audit script).
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:
- Ensure iCloud Desktop & Documents sync is enabled (System Settings โ Apple ID โ iCloud โ iCloud Drive)
- 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
- Clone repos to
~/code/:sv-portal,sv-edge,sv-db,sv-docs,sv-website,sv-wiki - Install Node.js via Homebrew:
brew install node@24(do NOT use .pkg installer) - Install CLI tools:
brew install supabase gh stripe deno pipx - 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-codecask 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-codeis deprecated in favor of native installers. - Install remaining apps directly: Docker Desktop (required for Supabase CLI), 1Password (biometric agent for
opCLI), 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 - Run
op signinto authenticate 1Password
App Update Strategy (as of March 2026)
Desktop apps are managed in three tiers:
- Homebrew casks (25 packages): Updated via
brew upgrade --greedy. The--greedyflag is required because most casks are markedauto_updates, which plainbrew upgradeskips. Includes desktop apps, CLI tools, and fonts. - App Store apps: Updated via
mas upgrade(requires password in terminal). - 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):
- 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"
- 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
- Verify migrations:
supabase migration list --linked - Read context:
~/.claude/CLAUDE.mdand~/code/CLAUDE.md - Docker Desktop must be running for Supabase CLI
Session End Protocol
- Run
sv-audit(orsv-checkfor full platform verification) โ do not declare clean untilAudit: CLEAN - If the audit flags GitHub hygiene issues (stale PRs, orphan branches): review and resolve
- 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.