# Vibn — Backend Handoff Tickets (for Sonnet)

> Companion to `VIBN_PRODUCT_BLUEPRINT.md`. These are the **backend / plumbing**
> tasks to hand to a cheaper model (latest Sonnet, standard reasoning; use high
> reasoning only on T2/T3/T6, which Opus should also review).
>
> Rule of the seam: the frontend is already built against typed contracts +
> mock data. **Implement endpoints to the exact shapes; do not change the
> contract types or the frontend components.** When an endpoint is live, swap the
> mock call for a fetch — that's the only frontend edit allowed.
>
> Disjoint write scope: these tickets touch `app/api/**`, `lib/**`, the agent
> runner, and the prompt files — NOT the onboarding `.tsx` UI (except the one
> documented mock→fetch swap in T11).

---

## Milestone 0 — Foundation (do first; nothing is safe until these land)

### T1 — One task ledger: markdown everywhere · ⚠️ Opus review
**Problem:** three prompts disagree on task tracking (route.ts says `plan_*` are
retired; the agent-runner `coder.ts` says call `plan_task_complete`; the session
runner toggles markdown). This causes the "loops on task 1" bug.
**Do:** make `.vibncode/specs/*.md` markdown checkboxes (`- [ ]` / `- [x]`) the
single source of truth in all three. Retire the DB `plan_*` tools (or make them
thin markdown writers). Ensure `.vibncode/` is committed and never removed by
`git clean -fd`.
**Files:** `vibn-frontend/app/api/chat/route.ts`, `vibn-agent-runner/src/prompts/coder.ts`, `vibn-agent-runner/src/agent-session-runner.ts`.
**Accept:** a delegated run that completes a task flips the markdown checkbox AND
the desktop Interactive Backlog reflects it; no prompt references `plan_*`.

### T2 — Extract BASE + MODE prompt modules · ⚠️ Opus review
**Do:** factor the shared prompt into `BASE` (identity, voice, spine/task-ledger
contract, infra model, hard rules, untrusted-content rule, project state) +
three `MODE` deltas (Collab / Build / Grow). Both `route.ts` and the agent-runner
import the same modules. See blueprint §5.3.
**Accept:** one source of truth for BASE/MODE; route + runner import it; Architect
("Collab") no longer contains the code/deploy body.

### T3 — MODE_TOOLS map + enforced gating · ⚠️ Opus review
**Do:** one `MODE_TOOLS: Record<"collab"|"build"|"grow", ToolName[]>` (blueprint
§5.2). Filter exposed tool schemas per mode in the prompt builder AND reject
out-of-mode calls in the dispatcher. Apply in route + runner.
**Accept:** Collab cannot call `ship`/`shell_exec`/`apps_create` (not in schema +
dispatcher rejects); `market_research_run` only callable in Collab; tool count
per turn drops to ~20–30.

### T4 — Phantom-tool + template-literal fixes (mechanical)
**Do:** in `route.ts`: `apps_envs_set`→`apps_envs_upsert`; `apps_containers_list`→
`apps_containers_ps`; remove `plan_decision_log` (doesn't exist); un-escape the
`\${activeProject.slug}` at ~L306 so it interpolates.
**Accept:** every tool name in prose exists in the registry; no literal
`${activeProject.slug}` in the compiled prompt.

### T5 — De-contaminate hardcoded specs
**Do:** the 10-file spec manifest in `route.ts` (~L346–357, COPPA/Missinglettr/
Dracula) ships to every user. Derive it from the active project, or replace with a
generic "read whatever exists in `.vibncode/specs/`."
**Accept:** a fresh project's prompt contains no GetAcquired-specific spec names.

### T6 — Metering ledger foundation · ⚠️ Opus review
**Do:** a per-event usage ledger `{ workspaceId, clientId, projectId, costType,
quantity, rawCost, ts }` (blueprint §8.2). Emit an event from every cost-incurring
tool (AI tokens, deploys, domains, market research, media, Missinglettr). Build on
`lib/quotas.ts`.
**Accept:** every billable action writes a ledger row tagged by project; a query
can total raw cost per client per period. (Invoicing UI is T16 — not now.)

---

## Milestone 1 — Onboarding + dashboard endpoints (implement to the contracts)

> Contracts: `vibn-frontend/app/(onboarding)/onboarding/onboarding-agency-types.ts`.
> Mock to replace: `…/onboarding-agency-mock.ts`.
> Flow reminder: onboarding ends at **ideal customer → dashboard**; the targeting
> recommendation lives in the **dashboard** (T8), not onboarding. Steps are:
> Identity (T7b) → Presence → Ideal Customer (T7) → POST (T9) → Dashboard (T10).

### T7 — POST /api/agency/analyze-expertise  `{ text }`
**Returns:** `{ tools: string[] }`. **Do:** an LLM call that maps the consultant's
free-text ideal customer / problem description to canonical tool-category
labels that match `smb_to_software_mapping` (so they join in T8). The FE mock is
`extractTools()` (keyword stub) — replace with the LLM, keep the output shape.
**Accept:** "I want to help dentists automate booking" -> `["Appointment Scheduling
Software"]` (or better); labels exist in the mapping. Matches them with potential customers in their area, and drives brand awareness.

### T7b — GET /api/agency/cities?q= (city autocomplete)
**Returns:** `CityRef[]` (global). **Do:** proxy **Places API (New) Autocomplete**
(`places:autocomplete`, restrict to localities/cities) for predictions, then
**Place Details (New)** to resolve each into `CityRef` (name = locality,
region = admin area level 1 short name, country + `countryCode` = ISO alpha-2,
lat/lng = location). Key stays server-side. The frontend `CityLookup` already
calls this and falls back to the seed list when absent.
**Accept:** typing "Vic" returns Victoria BC *and* global matches (not a fixed list).

### T7c — POST /api/agency/places/search  `{ name: string, city: CityRef }`
**Returns:** `PlaceMatch[]` (top 3 matching businesses).
**Do (Three-Stage AI Category Resolver):**
1.  **Stage 1: Google Places Lookup (Physical Context):**
    - Query **Places API (New) Text Search** using `GOOGLE_PLACES_API_KEY` with `${name} ${city.name} ${city.region}` to find matching business entities.
    - Extract: `id`, `displayName`, `formattedAddress`, `primaryType` (e.g., `"health"`), and `types`.
2.  **Stage 2: DataForSEO Website Lookup (Digital Context):**
    - If the business has a website, query **DataForSEO's OnPage/Database APIs** (or scrape the URL, falls back to raw query if offline) to retrieve the website's meta-title, description, and domain tags.
3.  **Stage 3: AI Assessment (The Reasoning Bridge):**
    - Feed both Stage 1 (Google Places categories/types) and Stage 2 (website title, description, domain tags) into a **Gemini LLM call** (`gemini-3.5-flash`).
    - **Prompt:**
      ```
      Google Places details:
      - Name: {{displayName}}
      - Primary Type: {{primaryType}}
      - Types: {{types}}
      
      Website Details:
      - Title: {{scrapedTitle}}
      - Description: {{scrapedDescription}}
      
      Based on the above, select the single most relevant category ID (gcid) for this business from our canonical mapping list. Return only the raw GCID string (e.g., "gcid:dental_hygienist" or "gcid:plumber").
      ```
    - Map the returned GCID to one of our 5 baseline categories: `"service"` (trades/FSM), `"appointments"` (scheduling), `"food"` (dining), `"retail"` (pos/shop), or `"events"`.
    - Retrieve the matched GCID's exact `"softwareNeeds"` list from `smb_to_software_mapping_final.json` and return it in `presetTools`.
**Accept:** looking up "Wheely Clean" (which Google labels `"health"`) correctly maps to `"gcid:dental_hygienist"` via AI assessment (by reading their teeth whitening website title), loading their actual dental scheduling, billing, and EHR/EMR custom blocks on Step 2! Shows loading spinner during execution.

### T8 — POST /api/agency/targets  `{ city: CityRef, tools: string[] }`  (powers the dashboard)
**Returns:** `TerritoryOpportunity[]`, sorted by `opportunityScore` desc.
**Do:** intersect `tools` with each SMB type's `softwareNeeds`
(`smb_to_software_mapping_final.json`) to pick candidate niches; for each, get
**business counts via the Places Aggregate API** (`:computeInsights`,
`INSIGHT_COUNT`) filtered by the niche's Places `type` (mapped from `gcid`)
within the city's area (circle around `city.lat/lng`). `opportunityScore` =
demand × weak/no-software gap × low Vibn saturation, biased by tool-fit. Treat
"Reporting / Dashboard Software" as a **universal** need. Mirror `mockTargets`;
set `matchedTools` per result.
**Accept:** real per-city counts for any city worldwide; `vibnClaimedCount` from
our DB; honest numbers only (no fabricated scarcity — blueprint honesty guardrail).

### T9 — POST /api/agency  `AgencyOnboardingResult`  `{ profile, expertise, tools }`
**Returns:** `{ workspaceSlug }`.
**Do (DB Storage Spec):**
1. **Workspace row:** Insert a new row into `fs_workspaces`.
   - `name` = `profile.name`
   - `slug` = derived slug from `profile.name` (idempotently deduplicated)
   - Store all metadata inside a structured `agency_onboarding` JSONB field in `fs_workspaces.data` (or related column):
     ```json
     {
       "city": {
         "id": "victoria-bc",
         "name": "Victoria",
         "region": "BC",
         "country": "Canada",
         "countryCode": "CA",
         "lat": 48.4284,
         "lng": -123.3656
       },
       "hasWebsite": true,
       "websiteUrl": "yoursite.com",
       "hasSocials": true,
       "hasBlog": false,
       "hasCustomDomain": false,
       "hasExistingClients": false,
       "expertise": "I want to help dentists automate booking",
       "tools": ["Appointment Scheduling Software"]
     }
     ```
2. **Workspace member row:** Link the signed-in NextAuth user (`userId`) as the `'owner'` of this workspace in `fs_workspace_members`.
3. **Provisioning:** Trigger the standard workspace provision pipeline (Gitea org, Coolify project boundary via `lib/workspaces.ts`) asynchronously so the tenant stands up.
**Accept:** round-trips the posted result; a new row is created in `fs_workspaces` and `fs_workspace_members`; metadata is saved perfectly in JSONB; and the workspace slug is returned.
*(No pitch, no claimed territory — those were removed from onboarding.)*

### T10 — The dashboard (the screen they land on)  · ⚠️ Opus may build
**Do:** the agency dashboard at `/[workspace]` (light paper/ink theme). On load,
call T7 (analyze-expertise, or use stored `tools`) + T8 (targets for their city)
and render the **recommended local businesses to target** (the gold-rush list
with businesses / weak-software / claimed + matched-tools chips). Plus clients/
prospects, projects, retainer MRR. Claiming a target creates a prospect.
**Accept:** lands from onboarding seeded with their ideal-customer description; shows real
recommendations; claim → prospect. *(FE craft — likely an Opus task; reuses
`extractTools` + `mockTargets` until T7/T8 are live.)*

### T11 — Wire onboarding + dashboard to the endpoints
**Do:** `CityLookup` already calls `GET /api/agency/cities` (T7b) with a seed
fallback — just stand up the route. Implement `finishAgency` in `page.tsx` to
POST T9 and route to `/[workspace]`. In the dashboard, swap `extractTools`/
`mockTargets` for T7/T8. No styling changes to onboarding.
**Accept:** the flow runs end-to-end on real data; onboarding behavior unchanged.

### T12 — Preserve homepage intent through auth
**Do:** if the homepage hero captures input, persist it across Google OAuth
(localStorage/draft, like `vibn:firstName`) so onboarding resumes seeded.
**Accept:** typing on the homepage → sign in → onboarding has the value.

---

## Milestone 2 — Design-first delivery (the custom tool)

### T13 — Ingest the 4 design-kit families
**Do:** register `vibn-ai-templates`, `vibn-app`, `vibn-crm`, `vibn-marketplace`
(in `design-templates/VIBN (2)/`) into the design-kit registry: one kit per family,
themes as overrides; add `DESIGN.md` + `tokens.css` (+ `SKILL.md`) per the existing
`lib/scaffold/open-design/design-systems/<id>/` structure.
**Accept:** `get_design_template` returns each; they appear on the Design tab.

### T14 — Build recipe: scaffold-from-kit first
**Do:** rewrite the Build mode recipe so building a client's **custom tool**
starts from a kit (fork into the client repo) + token reskin, instead of
`create-next-app`. The tool is scoped from the consultant's expertise + the
client's `softwareNeeds`; SMB domain → family; client brand → accent.
**Accept:** a build starts from a polished themed template, not an empty Next app.

### T17 — Onboarding hardening note (low priority)
**Do:** `page.tsx` has pre-existing unused imports (`useState`/`useEffect`/
`useMemo` on line 3) flagged as warnings — not from the agency work. Clean up if
touching the file.
**Accept:** no behavior change.

---

## Milestone 3+ — Grow & billing (later)

### T15 — `missinglettr_*` tool wrapper
**Do:** wrap the Missinglettr API (`workspaces.create`, `posts.create`, analytics).
Grow mode only. **Accept:** can schedule a multi-platform post; metered (T6).

### T16 — Stripe retainers + invoicing
**Do:** on the metering ledger, roll up cost → apply pricing (retainer / cost-plus /
fixed) → Stripe one-off invoice + recurring subscription for retainers.
**Accept:** an agency can invoice a client and start a monthly retainer.

### T18 — Google Business Profile (GMB) OAuth & Token storage
**Do:** add `https://www.googleapis.com/auth/business.manage` to the NextAuth Google Provider config. 
Upon user sign-in, save the authorized OAuth `access_token` and `refresh_token` in `fs_users.data`.
On the backend, write a helper to list GMB locations for the authorized user and support posting Google Local Business posts.
This is the core engine for automated GBP posting and review management in Grow mode.
**Accept:** signing in with Google requests the GMB permission; tokens are securely saved in `fs_users.data` and are queryable by the server.

### T19 — DataForSEO OnPage API Website Auditor
**Do:** implement a backend helper to post and retrieve data from **DataForSEO's OnPage API** (`/v3/on_page/task_post` -> `/v3/on_page/summary`).
Extract domain-wide metrics: `domain_info.cms` (to auto-detect what builder they are renting), `domain_info.ssl_info`, `page_metrics.broken_links`, and favicon availability.
**⚠️ Hard constraint:** DataForSEO's OnPage crawler strictly requires the target URL to include the protocol (e.g., must be `"https://allardcontractorsltd.com"` or `"http://..."`, NOT a bare domain). Ensure the server-side payload prepends `"https://"` automatically when creating the crawler task.
Expose this audit dataset to the dashboard so consultants can auto-generate SEO health audits for their prospects.
**Accept:** posting a scan request triggers the DataForSEO crawl; returns unified CMS, SSL, and link metrics; tags them to the client's project row.

---

## Notes for the implementer
- Don't touch the onboarding `.tsx` files except T11's documented swap.
- Keep `onboarding-agency-types.ts` as the contract; if a shape must change, change
  it there and flag it (the UI depends on it).
- Honesty guardrail (T8/T9): never show fabricated market/scarcity numbers.
- Flag T1/T2/T3/T6 for an Opus review pass before merge.
