michiu-marketing
Brand operations + AI marketing system for Restaurant Michiu — Cantonese-Japanese, Amsterdam-Zuid.
This repo is the internal command center: review management, social publishing, DM inbox, loyalty program ops, and the Marketing OS that orchestrates all of it. It owns the Supabase schema for the entire Michiu platform.
Who this is for
| Person | Role |
|---|---|
| Casper | Agency lead (Quarb Studio) — built and operates this system |
| Michelle Zhang | Owner-chef of Restaurant Michiu — the client. Approves brand decisions, signs off on copy. |
This is an internal agency tool, not a public product. There is no self-serve signup. Access is by invitation.
What Claude Code is
This project is built and operated using Claude Code — Anthropic's AI coding assistant that runs as a CLI in the terminal. When you open a terminal in this repo and run claude, Claude Code loads CLAUDE.md automatically as its operating contract. Every rule in that file governs every session.
Claude Code is not an IDE plugin and not a web interface. It's a terminal-native agent that reads files, writes code, runs commands, and commits — all from a conversation in your shell.
The most important rule Claude Code enforces here: nothing ever auto-posts. Every generated reply, caption, DM, or outreach draft requires human approval before it goes anywhere near a public surface. This is hardcoded into the system architecture and the operating rules.
Three-repo architecture
| Repo | Purpose | Deploy |
|---|---|---|
michiu-website | Public-facing site | Cloudflare Pages |
michiu-marketing (this repo) | Brand ops + AI tooling | Vercel (marketing.michiu.quarb.com) |
michiu-operations (future) | Kitchen + service ops | TBD |
All three repos share one Supabase project. Schema migrations live here only — other repos consume the DB via the Supabase JS client.
What's shipped
The six lanes
Work is organized into six domain lanes. Each is an independent working context with its own operating rules, handoff doc, and AI skill.
| Lane | What it does | Status |
|---|---|---|
| reviews | Brand-voice reply generation for Google, TheFork, TripAdvisor reviews. Multi-platform inbox, bulk paste, guest photos, Circle member linkage. | Production |
| social | Caption generation (with vision), asset library, cross-platform scheduler, Meta + LinkedIn publishers. | Code-complete — gated on Meta/LinkedIn app approvals |
| dm-inbox | Messenger/Instagram DM handling with brand-voice replies, Circle auto-link, bulk-paste tester. | Code-complete — gated on Meta pages_messaging approval |
| guest-intel | Customer + reservation store (Zenchef integration). Foundation shipped; Zenchef B2B client pending API docs. | Foundation (OS-31) |
| ads | Google Ads GCLID capture, conversion upload, Customer Match. | Scaffolded — gated on Google Ads dev token |
| marketing-os | Orchestration umbrella over all five lanes above. See below. | Epoch 2 complete |
The Marketing OS
The Marketing OS is an AI operating system built on top of the six lanes. It consists of:
- 12 helpers — named AI personas, each owning a domain:
Sun(Strategist),Zuck(Social),Siren(Replier),Anna(Concierge/DM),Penn(Editor/Press),Carlos(Local/SEO),Roy(Ads),Clara(Lifecycle/CRM),Insight(Analyst),Julius(Brand Guardian),Dion,Pablo
- Brand Memory — structured tables (
brand_identity,brand_voices,brand_banned_phrases,brand_audiences, etc.) that ground every helper output in Michiu's brand voice - Power-Ups — task-specific tools inside each helper (e.g. Sun's "RAI opportunity scan", Penn's "press pitch generator", Clara's "post-visit follow-up")
- Universal approval queue — every helper output lands here before anything reaches a public surface
- Inngest event bus — async orchestration between helpers (no direct helper-to-helper calls)
- Vault sync pipeline — Obsidian brand vault → GitHub → webhook → Inngest → pgvector (Brand Memory RAG)
The Circle of Michiu
Michiu's loyalty program — an invite-only membership that recognizes returning guests. It is felt, never announced: members are recognized when they walk in, remembered between visits, invited to private evenings. It never appears publicly by name. In the codebase it surfaces as circle_members table + circle_tier field on reviews.
Hard rules (a newcomer must know these)
These live in CLAUDE.md in full. The four that will cause the most damage if broken:
- Never modify an applied migration. Migrations are append-only once shipped. Add a new file; never edit an existing one. Breaking this corrupts the schema history for all three repos.
- Nothing auto-posts. Human approval is required before any generated content reaches a public surface. No exceptions until an eval harness is built and passes.
- Every new database table needs
account_id text not null default 'michiu'. This is the SaaS seed — without it, future multi-tenancy requires painful backfills. - Every LLM-generated public-facing string runs through
checkBannedPhrases(). This is layer 2 of brand voice protection. Never remove it.
Dev setup
Two locations, one codebase
Mac (primary) iPad / iPhone (mobile)
───────────────────────────────── ─────────────────────────────────────
Repo: ~/APP-STUDIO/MICHIU_Code/ Blink app → Mosh → Hetzner devbox
michiu-marketing ubuntu-4gb-nbg1-1 · 46.225.238.8
Claude Code: runs locally Claude Code: runs on devbox
Repo on devbox: ~/projects/michiu/
michiu-marketing
iPad and iPhone both SSH into the same devbox — they share one filesystem and one Claude Code memory store. There are two Claude project slugs, not three:
| Location | Claude slug |
|---|---|
| Mac | -Users-CasperMacbook-APP-STUDIO-MICHIU-Code-michiu-marketing |
| Devbox (iPad + iPhone) | -home-casper-projects-michiu-michiu-marketing |
What is Tailscale?
Tailscale is a VPN that connects the Mac and the Hetzner devbox on a private network. The devbox has no public SSH port open — all access goes through Tailscale. If Tailscale is not running, or if WARP (Cloudflare) is active on the Mac (they conflict), the devbox is unreachable. Use the direct IP 100.112.135.68 if the .ts.net hostname fails to resolve.
Session handoff protocol
git pullbefore starting any sessiongit pushbefore switching devices- Paste a brief "where I left off" between terminal windows when switching
- After a devbox session, sync Claude Code memory to Mac:
# Run on Mac — pulls devbox memory across Tailscale
bash scripts/devbox/sync-from-server.sh 100.112.135.68
Connecting to the devbox
# Via Tailscale hostname (preferred)
ssh casper@ubuntu-4gb-nbg1-1.ts.net
# or direct Tailscale IP if hostname doesn't resolve:
ssh casper@100.112.135.68
# Mosh (better for mobile — survives network switches and sleep)
mosh casper@100.112.135.68
tmux on the devbox
The devbox runs tmux to keep Claude Code sessions alive across disconnects. Named sessions are pre-created per project:
tmux attach -t michiu # re-attach to the michiu session (Claude Code running here)
tmux ls # list all active sessions
From Blink on iPad/iPhone: connect via Mosh, then tmux attach -t michiu. If Claude Code was mid-task when you disconnected, it resumes exactly where it left off.
Mac local does not need tmux. There's no SSH connection to drop and the process survives sleep natively. Just run claude directly in any terminal window (iTerm2, Warp, Terminal.app).
Tech stack
| Layer | Choice | Why |
|---|---|---|
| Language | TypeScript (strict mode) | End-to-end type safety across DB → API → UI |
| Framework | Next.js 15 (App Router) + React 19 + Tailwind 4 | Admin console + reference site in one app |
| Database | Supabase (Postgres + RLS + Auth) | Managed Postgres with row-level security per account |
| LLM | Anthropic SDK — claude-sonnet-4-6, prompt-cached | Brand-voice generation; prompt caching cuts cost on repeated system prompts |
| Event bus | Inngest | Async helper orchestration; retries, DLQ, step checkpointing built in |
| Resend | Transactional alerts + Clara lifecycle outreach | |
| Validation | Zod | Runtime schema validation on all API inputs |
| Lint + format | Biome | Single tool replaces ESLint + Prettier |
| Package manager | pnpm | Faster installs, strict lockfile |
| Node | 22 (.nvmrc) | |
| Deploy | Vercel (admin + API) + Cloudflare Pages (reference static export) |
Local setup
git clone git@github.com:quarbstudio/michiu-marketing.git
cd michiu-marketing
pnpm install
cp .env.example .env.local
# Fill in the values — see .env.example for all required vars
pnpm typecheck
pnpm dev
Admin runs at http://localhost:3000/admin. The full env var list is in .env.example — the minimum to get started is NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and ANTHROPIC_API_KEY. Without Supabase vars, the review system falls back to a file-backed mock store (.data/reviews.json); everything else will error.
Key scripts
pnpm dev # local dev server
pnpm typecheck # tsc --noEmit
pnpm lint # Biome check
pnpm regress # review prompt regression suite (7 worked examples)
pnpm regress:dm # DM prompt regression suite
pnpm seed # seed mock review store
pnpm seed:guests # seed guest graph (customers + reservations)
pnpm import:google # import Google reviews via API
pnpm import:csv # import reviews from CSV
pnpm import:zenchef # import Zenchef Manager UI CSV export
pnpm cron:tick # manually fire due scheduled posts (Hobby-tier workaround)
pnpm vault:push # push Obsidian vault → GitHub mirror (Mac only)
pnpm cf:build # static export for Cloudflare Pages reference site
Apply schema migrations
supabase login
supabase link --project-ref YOUR_PROJECT_REF
supabase db push
108 migrations applied (0001 → 0108). Full annotated list in HANDOFF.md → "Database schema". Never edit an applied migration file — add a new one.
CI/CD
GitHub Actions runs on every PR: pnpm typecheck + pnpm lint. Both must pass before merging. Config at .github/workflows/ci.yml.
Repo structure
michiu-marketing/
├── CLAUDE.md ← operating contract — Claude Code auto-loads this at session start
├── HANDOFF.md ← current state + next steps — read this second
│
├── src/
│ ├── app/
│ │ ├── admin/ ← admin console (local + Vercel) — /admin/reviews, /admin/posts, etc.
│ │ └── reference/ ← marketing reference site (Cloudflare Pages static export)
│ ├── brand/ ← review-prompt.ts + caption-prompt.ts + dm-prompt.ts (core brand-voice IP)
│ ├── marketing-os/ ← 12 helpers, Brand Memory, Power-Ups, Campaign, Inngest functions
│ └── lib/
│ ├── reviews/ ← review pipeline + store
│ ├── guest-graph/ ← customer-store + reservation-store
│ └── integrations/ ← google/, zenchef/, csv/
│
├── docs/ ← knowledge base (brand, strategy, competition, channels, RAI, circle)
├── supabase/migrations/ ← 108 migrations — append-only after applied
├── scripts/ ← seed, import, regress, cron, devbox sync utilities
├── brand-assets/ ← canonical visual asset library (logos, photos, video, templates)
├── platforms/ ← per-platform config + sized image exports (GBP, Instagram, LinkedIn, etc.)
├── editorial/ ← press kit + editorial opportunity tracker
│
└── .claude/
├── lanes/ ← per-lane CLAUDE.md (operating rules) + HANDOFF.md (current state)
└── skills/ ← auto-triggering domain skills loaded by Claude Code
Lane operating rules
Each lane has two files a new developer must read before touching that lane:
| Lane | Operating rules | Current state |
|---|---|---|
| reviews | .claude/lanes/reviews.CLAUDE.md | .claude/lanes/handoffs/reviews.HANDOFF.md |
| social | .claude/lanes/social.CLAUDE.md | .claude/lanes/handoffs/social.HANDOFF.md |
| dm-inbox | .claude/lanes/dm-inbox.CLAUDE.md | .claude/lanes/handoffs/dm-inbox.HANDOFF.md |
| guest-intel | .claude/lanes/guest-intel.CLAUDE.md | .claude/lanes/handoffs/guest-intel.HANDOFF.md |
| ads | .claude/lanes/ads.CLAUDE.md | .claude/lanes/handoffs/ads.HANDOFF.md |
| marketing-os | .claude/lanes/marketing-os.CLAUDE.md | .claude/lanes/handoffs/marketing-os.HANDOFF.md |
Session start sequence: CLAUDE.md → HANDOFF.md → lane CLAUDE.md → lane HANDOFF.md → start work.
Knowledge architecture
Code lives here. Strategy lives in three Obsidian vaults:
| Vault | Location | What |
|---|---|---|
| MICHIU | iCloud → Documents/MICHIU/ | Brand, marketing, ops, guests — canonical source of truth for WHY decisions were made |
| QUARB | iCloud → Documents/QUARB/ | Quarb Studio agency operations |
| UGC_AGENCY | iCloud → Documents/UGC_AGENCY/ | Separate AI UGC venture |
The MICHIU vault syncs to GitHub (quarbstudio/michiu-brand-vault, private) every 15 minutes via a Mac LaunchAgent (scripts/vault-sync.sh). A GitHub webhook triggers an Inngest pipeline that chunks and embeds the vault docs into memory_documents (pgvector) — this is the RAG layer the Marketing OS helpers use to ground their outputs in Michiu's brand strategy.
The vault is the source of truth for why. This repo is the source of truth for what runs.
Approval workflow
Every AI-generated output — review replies, social captions, DM replies, press pitches, lifecycle emails — lands in the universal approval queue at /admin/marketing-os/approvals before anything leaves the system. The flow:
Helper generates draft
→ Julius (Brand Guardian) runs a pre-publish rubric check
→ Draft lands in pending_approvals with warnings (advisory only — never blocking)
→ Human reviews → approves / rejects / regenerates
→ On approval: content routes to its destination (reply posted, post scheduled, email queued)
There is no bypass. No draft auto-advances to published. This is enforced in code, not just policy.
License
Proprietary — Restaurant Michiu, Amsterdam. Quarb Studio is the agency operating the marketing systems.