Porchlight CRM

CRM app · built by the Vibecode team

Porchlight opens on Today for a fictional two-agent team in the Phoenix area: today's showings with Directions and Confirm, overdue follow-ups, new leads with a speed-to-lead timer, and contract deadlines. Contacts keep buyer criteria, pre-approval and matching listings, and every listing gets a public page where buyers request a showing that lands straight in the CRM. A showing scheduler catches double bookings, the pipeline runs from Lead to Closed with commission and your share on every card, and call mode suggests the next follow-up. Import with undo, reports and settings all save to a real database, the demo logins sit on the sign-in page, and one click removes the demo.

  • Web app
  • Built in 4 prompts
  • Free to remix
Porchlight CRM, crm app built with Vibecode
Remix this template

Remix copies the working app into your account for free, exactly as we built it. It runs in your browser.

The prompt that builds it

We built Porchlight CRM in 4 prompts. Here they are folded into one, so a single paste into a new app builds the finished version.

Prompt · 9,567 words

Build "Porchlight CRM": the CRM for real estate agents. It is for a solo agent or a small team (the lead agent plus a buyer's agent or assistant) who tracks buyers, sellers and listings in a spreadsheet, their phone's contacts and a paper showing calendar, often from the car between showings. The promise, in paying users' words: "The dashboard should have tasks to do featured at the top." and "we are trying to take all the thinking out of booking showings." and "when I open the app, I should immediately know who I need to contact and what I need to do next." Put today's showings and the next useful action first on every screen. Do not make a generic admin dashboard. It ships with a fictional demo workspace, "Castillo Home Group", a 2-agent team in the Phoenix metro, Arizona.

First download every reference screenshot with curl into refs/ in the workspace and look at each one before writing code. Borrow layout, density and mechanics only; use our colours, not theirs (no Follow Up Boss blue header, no Redfin red, no purple sidebars):
- https://webtpl-real-estate-crm-refs.beacon.host/01-linear-list.jpg : Linear "My issues". Warm grey canvas, sidebar sitting on the canvas, content in one white inset panel with a 1px border, dense 40px rows, pill tabs, quiet grey meta text. Every list in the app should feel this restrained.
- https://webtpl-real-estate-crm-refs.beacon.host/02-linear-detail.jpg : Linear split view. Activity timeline in the middle, a "Properties" card on the right. The model for Contact detail.
- https://webtpl-real-estate-crm-refs.beacon.host/03-attio-table.jpg : Attio record table: initial badge plus name, small chips, a count footer. The model for the Contacts table.
- https://webtpl-real-estate-crm-refs.beacon.host/04-attio-tasks.jpg : Attio Tasks grouped "Today / This week / Upcoming" in grey group bands, amber "Due today", red "Due 4 days ago". The model for the Today follow-up list.
- https://webtpl-real-estate-crm-refs.beacon.host/05-pipedrive-board.jpg : Pipedrive deals board: each column header shows total value and deal count; cards show title, person, value and a warning icon when there is no next activity. Pipeline information.
- https://webtpl-real-estate-crm-refs.beacon.host/06-pipedrive-lead-detail.jpg : Pipedrive lead detail. Note the "Focus" box with one-tap follow-up chips "In 1h · In 3h · Tomorrow · Next week". The model for our next-follow-up chooser.
- https://webtpl-real-estate-crm-refs.beacon.host/07-fub-smart-lists.jpg : Follow Up Boss smart lists: big tabs with counts "All People (127) · Hot (4) · Warm (2) · Cold (2) · Past clients (3)", then "Showing all 4 people" and a filter icon. The model for Contacts smart lists.
- https://webtpl-real-estate-crm-refs.beacon.host/08-fub-deal-board.jpg : Follow Up Boss deal board: a thin coloured bar on top of each column, the stage name, plain cards with people. Pipeline cleanliness.
- https://webtpl-real-estate-crm-refs.beacon.host/09-fub-tasks.jpg : Follow Up Boss tasks "Today's Tasks · Overdue (3) · Future (3)" grouped by day ("Thursday, Jul 6 (1)") with the person's name and the task ("Check in day after showing"), plus the phone agenda. Today and the phone agenda.
- https://webtpl-real-estate-crm-refs.beacon.host/10-fub-appointment.jpg : Follow Up Boss "Create Appointment" dialog: "Showing at 133 Pinwood", date, start and end time, location, guests. The model for the showing scheduler dialog.
- https://webtpl-real-estate-crm-refs.beacon.host/11-listing-page.jpg : a real listing page: a big photo plus a 2x2 photo grid, "For sale" dot, price, "3 bd · 2.5 ba · 2,438 sq ft", the address, and a right card with "Request showing" as the primary button and "Ask a question" plus a phone number. The model for our public listing page (our colours).
- https://webtpl-real-estate-crm-refs.beacon.host/12-fub-mobile-contact.jpg : a phone contact screen: name, stage, "Last communication", tabs, round Call/Text/Email buttons. The phone Contact detail.

## Stack

Use the platform's standard setup: the React + Vite webapp, the Hono backend on Bun, and Prisma (SQLite) + Better Auth from the database-auth skill. Real accounts and a persistent database. Put API contracts as Zod schemas in backend/src/types.ts. Use relative /api URLs so the app works on any domain, and configure Better Auth so sign-in works on a custom domain from day one (trust the request's own origin; never hardcode the preview host), which avoids "Invalid Origin" errors. Sessions last 30 days. Remixes get a copy of backend/.env with BETTER_AUTH_SECRET cleared, so at startup, before the environment is validated, generate a secret when it's blank and save it to backend/.env. Schema changes are additive only: never drop or rename a column. All money is integer cents. All timestamps are stored in UTC and shown in the workspace timezone. Every table has workspaceId, isDemo, createdAt and archivedAt. Store JSON as text. Use lucide-react icons (16px, 1.75 stroke) and recharts for charts. Keep every source file small (under ~250 lines): one file per screen section, component and route group.

The database must live in git: keep backend/prisma/dev.db (and its -wal/-shm files when they exist) committed, and never add dev.db, *.db or the -wal/-shm/-journal files to any .gitignore. The platform saves an idle workspace with `git add --all`, so an ignored database would lose a user's data. Never store a visitor's IP address or user agent: blank ipAddress and userAgent whenever a session is created or updated (a Better Auth databaseHooks.session hook is fine).

## Look and feel

- Light by default, with a dark toggle in the workspace menu.
- Colours: canvas #F7F6F2; panels and cards #FFFFFF; ink text #1C1B19; muted text #6B6760; borders 1px #E7E4DD; row hover #F3F1EC; selected row #FBEFE9. ONE action colour: terracotta #B4532A (hover #9C4521, focus ring rgba(180,83,42,.32)), white text on it. Status colours only for time: on time green #15803D on #ECFDF3, due today amber #B45309 on #FFFAEB, overdue red #B42318 on #FEF3F2. Stage dots (muted): Lead #9CA3AF, Showing #5B7DB1, Offer #B7791F, Under contract #7A5FA8, Closed #2F855A, Lost #9B5C5C. Listing status chips: Coming soon slate #475467 on #F2F4F7, Active green #15803D on #ECFDF3, Pending amber #B45309 on #FFFAEB, Sold ink #1C1B19 on #EDEBE6, Withdrawn muted. Temperature chips: Hot #B42318 on #FEF3F2, Warm #B45309 on #FFFAEB, Cold #475467 on #F2F4F7. Dark mode: canvas #12110F, panel #1A1917, border #2A2825, text #ECEAE6, muted #A19C94, accent #D06A3E.
- Type: Inter (weights 400/500/600) with tabular numbers on every number and price. Desktop body 14px, table text 13px, meta 12px muted, page title 15px/600 in a 52px top bar like Linear, strip numbers 18px/600, listing prices 20px/600. Phone body 15px and inputs 16px.
- Spacing on a 4px scale (4, 8, 12, 16, 24, 32). Corners: 8px on panels, cards and photos, 6px on buttons and inputs, full pills for chips. No shadows except menus, popovers, dialogs and the Cmd-K dialog (0 8px 24px rgba(28,27,25,.12)). No gradients, no glass, no emoji, no stock illustrations, no default-shadcn look.
- Buttons: primary terracotta with white text, 32px tall on desktop, 44px on phone. Secondary: white with a 1px border. Ghost for icon buttons.
- Avatars are initials in muted circles. No photos of people anywhere. House photos are AI-generated (see Listing photos).
- Dates are human: "Thu 3:15 pm", "Today 3:15 pm", "Yesterday 4:02 pm", "Oct 28", "in 3 days", "2h ago", "Closes in 18 days". Never seconds, milliseconds, ISO strings or internal IDs on screen.
- Money: "$985,000" for prices; "$985K" only in tight board cards and the strip; commission as "$24,625 · your share $19,700".
- Motion: 150ms ease-out for menus and drawers; rows fade out when completed; the call queue slides to the next contact. Respect prefers-reduced-motion.
- Brand: the product mark is a 20px terracotta (#B4532A) rounded square (5px radius) with a white roof chevron and a small white dot under it (a porch light), plus the wordmark "Porchlight". Use it as public/favicon.svg (replace any Vibecode favicon). Set <title> to "Porchlight CRM". Write a 1200x630 public/og.png (mark, "Porchlight CRM", "The CRM for real estate agents" on #F7F6F2) from an SVG with sharp or resvg, and point og:image and twitter:image at it. No Vibecode branding anywhere.

## Listing photos

Generate the house photos once, at build time, with gpt-image-2.5-flare through the project's proxied OPENAI_API_KEY (POST https://api.openai.com/v1/images/generations, model "gpt-image-2.5-flare", size 1536x1024). Write a small script in backend/scripts/ that generates them, then save each as a compressed JPG (1440px wide, under 250 KB, convert with sharp) in webapp/public/listings/<listing-slug>-<n>.jpg and commit the files. Never generate at runtime. Photorealistic Phoenix-area homes in daylight: stucco, tile or flat roofs, desert landscaping, palo verde trees, pools where the listing has one; bright, clean real-estate photography, wide angle, no people, no text, no logos, no house numbers or street signs. Our 9 listings get 3 photos each (front exterior, kitchen, living room or backyard). The 7 other-brokerage properties get 1 exterior photo each. If a generation fails, retry it once, then fall back to a neutral placeholder tile with the address, never a broken image. Every listing with photos shows a small "AI photo" caption at the bottom-left of the main photo.

## App shell

- Desktop: a 232px sidebar on the canvas, collapsible to a 56px icon rail (button at the bottom; remember the choice). Top: the workspace button "Castillo Home Group" with a chevron (menu: Settings, Dark mode, Sign out), then a search button "Search… ⌘K". Nav: Today (badge = overdue + due today + showings today), Contacts, Listings, Showings (badge = showings today), Pipeline, Calls (badge = new leads not yet called + calls due), Import, Lead capture, Reports. Bottom: Settings, then the signed-in user's initials, name and role.
- Wrap every route in an error boundary that keeps the shell and shows "This page didn't load. [Reload]"; retry a failed lazy chunk import once on its own before showing it.
- Main content sits in one white panel with a 1px border and 8px radius, inset 8px from the window edge, as in 01-linear-list. Each screen has a 52px top bar: the title on the left, the screen's actions on the right.
- Demo banner: while demo records exist, a 32px bar at the top of the panel, background #FFF8E6, text #8A5A00: "You're viewing demo data · Remove demo data" (the link opens the removal dialog).
- Cmd-K (and "/") opens a command palette: input "Search contacts, listings and deals, or type a command", grouped results (Contacts, Listings, Deals, Actions) with a type label column and right-aligned meta (stage, price or status). Actions: Schedule a showing, Log a call, New contact, New listing, Import a list, Go to Today/Contacts/Listings/Showings/Pipeline/Calls/Reports/Settings. Arrow keys, Enter, Esc.
- Phone (below 768px): no sidebar. A 56px top bar with the title, a search icon and a "+" button (menu: New contact, Schedule a showing, Log a call, New listing). A fixed bottom bar with 5 tabs, icon plus 11px label: Today · Contacts · Showings · Pipeline · More. "More" opens a sheet with Listings, Calls, Import, Lead capture, Reports, Settings, Dark mode and Sign out. Respect the safe-area inset. Every tap target is at least 44px.
- Security bar: while the workspace is unclaimed and holds any real (non-demo) record, show owners a slim amber bar under the demo banner: "Anyone can sign in with the demo logins. Create your owner account to make this workspace private." with a "Create my account" button that opens the claim sign-up.

## Screens

### 1. Sign in (/login) and account pages
- A centred 400px card on the canvas: mark plus "Porchlight", heading "Sign in", Email, Password, a terracotta "Sign in" button, "Email me a sign-in code instead" (the platform's email code flow, with a code page that has "Resend code" and a 30-second countdown), "Forgot password?" (reset by code), and "New here? Create your account" while the workspace is unclaimed.
- Below the form, a "Demo workspace" box (fictional data) that shows the logins so nobody is stuck on first run:
  - Owner · maya@castillohomes.example · password porchlight-demo · [Use]
  - Buyer's agent (sees only his own contacts) · ben@castillohomes.example · password porchlight-demo · [Use]
  - [Use] fills the form and signs in.
- Errors in plain English: "That email and password don't match. Try again, or email yourself a code." / "We couldn't send the code. Check the address and try again." / "That code has expired. We've sent a new one."
- Claiming: the first person who creates an account becomes the Owner and the workspace is marked claimed. From then on the two demo logins stop working (their sessions are revoked and the box disappears), and new people join only through an invite link. The demo records stay, still flagged demo, until the owner removes them.

### 2. Today (/), the home screen
- Top bar: "Today" plus the date ("Friday, November 13"). Actions: "Log a call" (secondary), "Schedule showing" (secondary) and "New contact" (primary).
- A thin strip, not big cards: per open stage (dot, stage name, deal count and volume, e.g. "Under contract · 3 · $1.6M"), then "Pending commission $40,425" (your share of under-contract deals) and "Closed this month $23,350 · 2 deals". Values come from the database.
- Left column:
  1. "Showings today" first, as a vertical agenda like the phone agenda in 09-fub-tasks: time ("10:00 am", "10:45 am"), the address (500 weight) and city, the buyer's name, a status chip (Requested amber, Confirmed green, Done grey), and buttons: Directions (opens https://www.google.com/maps/dir/?api=1&destination=<address> in a new tab), Text buyer, and for Requested showings a primary "Confirm". After a showing's end time, the row shows "Add feedback" instead. When there are none: "No showings today · Schedule one".
  2. "Follow-ups", grouped like 04-attio-tasks, in grey group bands with counts: Overdue (red label), Due today (amber), Next 7 days (sub-grouped by day: "Sat Nov 14"). Each row has two lines: line 1 is the contact's name (500 weight) followed by the full task title (readable up to 60 characters at 1440px, never cut to a few words); line 2 is the context (the property address or "Buyer · $650K-$725K · Chandler, Gilbert") · stage dot and label · the last touch ("Talked · wants a pool, no HOA · 2d ago"). Around them: a complete checkbox and a task-type icon (phone, mail, message, calendar, file) on the left, and on the right when ("Overdue 3d" red, "3:15 pm" amber, "Sat 10:00 am") · one-tap Call, Text and Email icon buttons · a "…" menu (Snooze 1 hour / Tomorrow / Next week, Reschedule, Download .ics, Open contact). Completing a call row asks for the outcome inline, then fades the row out.
- Right column (320px):
  - "New leads": listing-page and website leads nobody has called yet, each on two full lines: the full name and what they asked for ("Jordan Pike · Showing request · 4127 E Desert Willow Dr"), then the source chip ("Listing page" / "Home value form" / "instagram · social") and the age ("35m ago", turning red after 5 minutes with "Speed to lead"). Call (small secondary) and Open (ghost) sit on the right of line 1. Never ellipsize a name under 32 characters.
  - "Contract deadlines this week": from under-contract deals' checklists, each line "Appraisal due · 1129 W Juniper Ave · Mon" with the date chip coloured by due state, linking to the deal.
  - "Getting started" checklist (hideable), each item ticked automatically from real state: Add your first listing, Import your contacts, Put a listing page on your site, Invite your team, Remove the demo data. Each unticked item links to the place that does it.
- Phone: the strip scrolls sideways. The order is strip, Showings today, Overdue, Due today, Next 7 days, then New leads, Contract deadlines and Getting started. Each follow-up row stacks: the name (up to 2 lines) with the due label on the right, then the context line · stage, then the last touch on one line, then the Call, Text and Email buttons at 44px.

### 3. Contacts (/contacts)
- Top bar: "Contacts" plus the count; actions: Export CSV, "New contact".
- Smart lists as big tabs with counts like 07-fub-smart-lists: All (56) · Hot · Warm · Cold · Buyers · Sellers · Past clients · New leads · No next step · Archived, plus "+ Save list" (saves the current search and filters under a name). Counts come from the database.
- Filter row: a search box (matches name, email, phone digits, city, area and property address) and filter chips: Type (Buyer, Seller, Buyer & seller, Past client, Sphere), Temperature, Source, Agent, Pre-approved, Price range, Area, No next step, Do not contact, and "Clear". "Showing 56 contacts" under the tabs.
- Table (like 03-attio-table): checkbox · Name (initials badge, name, and the type below in muted text) · Temperature chip · Looking for / Selling (buyers: "$650K-$725K · 4 bd · Chandler, Gilbert"; sellers: the property address and list price) · Stage (dot plus label) · Source · Last touch · Next step (a coloured chip by due state, or an amber "No next step") · Agent (initials). Sortable headers. Footer: "56 contacts". The table fits at 1280px and wider without sideways scroll: Source shows only from 1600px (it stays in the filters), Last touch is capped at 220px with an ellipsis, and the Next step chip shows the task title (up to 26 characters) plus the due time. "No next step" (the smart list, the filter, its count and the amber chip) means a contact with an open deal (Lead, Showing, Offer, Under contract) or a Hot/Warm buyer or seller with no open task; past clients, sphere, Lost and do-not-contact contacts never count and show a muted "—" in the Next step column.
- Selecting rows shows a floating ink action bar at the bottom: "3 selected · Tag · Set temperature · Assign · Export · Archive · ✕".
- Below 640px the table becomes cards: name and temperature, the looking-for or selling line, stage, last touch, next step chip, and Call / Text / Email buttons.

### 4. Contact detail (/contacts/:id)
- Header: breadcrumb "Contacts / Priya & Dev Raman", the name (20px/600), the type ("Buyer"), a temperature picker chip, and a small "Demo" tag on demo records. A meta line: "Source: Zillow · Agent: Maya Castillo · Added Oct 2". Right side: a stage picker for the contact's main deal (a button with the stage dot that opens a menu of stages; no dragging needed); the next step as a chip ("Next: Showing tour · Today 10:00 am", click to edit or complete; amber "No next step · Add one" when missing); then a labelled primary "Call Priya" button (the first name, opens tel:), then Text and Email as icon buttons with tooltips, and a "…" menu: "Schedule a showing", "Archive contact" (Undo toast; archived contacts show only in Archived, with Restore) and, for owners, "Delete contact" (confirm dialog; removes the contact with its deals, tasks, activities, showings it owns and form submissions).
- Body in two columns, like 02-linear-detail:
  - Main column, tabs Timeline · Showings · Deals · Matches · Details.
    - Timeline: a composer at the top with tabs Log call / Note / Email (the call form is the same one as on the Calls screen). Entries newest first, each with an icon, actor and time: calls with outcome and duration; emails with subject and a status chip (Delivered, Bounced, Failed, Sent from your email app); texts logged from the Text button ("Texted from your phone"); notes; showings ("Toured 2871 S Rosemary Dr · Interest 4/5 · 'Loved the kitchen, backyard too small'"); stage changes ("Moved from Showing to Offer"); form submissions (fields, plus utm_source/medium/campaign); imports ("Imported from sphere-contacts.csv").
    - Showings: past and upcoming showings for this contact, each with the property, date and time, status and feedback, plus "Schedule a showing".
    - Deals: side (Buyer or Seller), property, price, stage, est. commission and your share, expected close, days in stage, "Add deal".
    - Matches (buyers): our active and coming-soon listings that fit the buyer's criteria (price within budget +5%, beds at least the minimum, city in their areas), each as a small listing card with "Schedule a showing" and "Text this listing" (an sms: link with the listing page URL in the body).
    - Details: emails and phones with labels, spouse/partner, mailing address, birthday and home anniversary (past clients), source, UTM tags, consent, tags, and the import batch it came from (link).
  - Right column (360px wide; select values and names never wrap mid-word, long values such as the lender, the property address or the motivation wrap onto two lines instead of being cut), a "Buyer criteria" card for buyers (budget min and max, areas, min beds, min baths, must-haves chips like "Pool", "Single story", "No HOA", "3-car garage", timeline "0-3 months", pre-approval: status Pre-approved / In progress / Not yet, lender, amount and expiry shown as text, "Dec 24, 2026 · in 41 days", or red "Expires in 6 days" when within 14 days, opening a date picker only when clicked; never a raw 12/24/2026 input), a "Selling" card for sellers (property address, est. value, listing appointment date, motivation, timeline, linked listing), and a "Properties" card: Agent, Source, Temperature, Tags (add or remove), Do not contact toggle. Every field edits in place and saves on blur with a small "Saved" tick.
- Phone: one column; the header stacks; Call/Text/Email are three round 44px buttons like 12-fub-mobile-contact; the tabs scroll; a sticky "Call Priya · (480) 555-0162" button sits above the bottom bar.

### 5. Listings (/listings) and listing detail (/listings/:id)
- Top bar: "Listings" plus the count; tabs: Our listings · Other brokerages (properties our buyers toured or offered on) · All; filters Status and City; actions "New listing" (primary).
- Grid of cards, 3 per row on desktop (1 on the phone): the main photo (16:10, 8px radius, "AI photo" caption), the status chip over the photo top-left, the price (20px/600), "4 bd · 3 ba · 2,840 sq ft", the address and city, then one muted line "12 days on market · 9 showings · 3 this week" and the seller's name. Coming soon cards show "Goes live Thu".
- Listing detail: a photo header like 11-listing-page (the big photo plus up to 2 more beside it; click opens a simple lightbox), then two columns:
  - Main: status picker (Coming soon, Active, Pending, Sold, Withdrawn), price with a price-history line ("Reduced $15,000 on Nov 6"), beds, baths, sq ft, lot, year built, garage, pool, HOA ($/mo or None), MLS # (an empty field labelled "MLS not connected"), description, features chips. Tabs: Showings (every showing with date, buyer or buyer's agent, status and feedback; a summary "9 showings · avg interest 3.6/5 · top comment: price feels high for the backyard") · Interested buyers (contacts who toured it or asked about it, plus buyer matches) · Activity · Public page.
  - Right card: seller (name, Call/Text), list date, days on market, list price, commission (rate and est. commission), open houses (date and time, "Add open house"), and the public page link with Copy and Open.
- New/edit listing: a dialog with the fields above, photo upload (stored in the backend, served from /api/files/...), and the seller picked from contacts or created inline. A new listing gets its public page at once.
- Public page tab: the hosted link (current origin + /l/<slug>), "Copy link", "Open", an iframe embed snippet with Copy, and "Showing requests from this page: 4 · questions: 2".

### 6. Showings (/showings): the scheduler
- Top bar: "Showings" with a week switcher (‹ This week ›, "Today"), a filter by agent, and "Schedule showing" (primary).
- Desktop: a 7-day week grid from 8:00 am to 8:00 pm, with showings as blocks coloured by status (Requested amber outline, Confirmed solid light terracotta #FBEFE9 with a terracotta left bar, Done grey, Cancelled struck through) showing time, address and buyer. A buyer's tour (several showings in a row) is grouped with a thin connector line and a label "Tour · 3 homes". Clicking a block opens a side drawer: property (photo, price, address, Directions), buyer (Call/Text), time, status buttons (Confirm, Mark done, Cancel with a reason), a "Text confirmation" button (sms: link with the filled-in quick text), "Download .ics", and after it's done, the feedback form.
- Below the grid (and the only view on the phone): an agenda list grouped by day, like the phone side of 09-fub-tasks. On the phone it starts at today ("Today · Fri Nov 13"); days of this week before today collapse into one "Earlier this week (7)" row at the top that expands on tap.
- Schedule showing dialog (like 10-fub-appointment): Property (search our listings and other-brokerage properties, or type a new address, which creates an "Other brokerage" property), Buyer (search contacts or create one inline), Date, Start time (15-minute steps; it defaults to the agent's next free slot from now that doesn't overlap their showings, or tomorrow 10:00 am when today has none before 6:00 pm) and Length (30 min default, 15/30/45/60), Agent, Notes for the buyer, and "Add another home to this tour" which adds a second and third property with start times pre-filled 15 minutes after the previous one ends. Conflict check: if the agent already has a showing that overlaps, show "Overlaps 10:00-10:30 am at 2871 S Rosemary Dr" in red under the time and disable Save until it's changed. Save creates the showing(s) as Confirmed, logs an activity on the contact, creates a "Get feedback" task for the next morning, and offers "Text Priya the tour" (an sms: link whose body lists the times and addresses).
- Feedback form (after a showing): Interest (1-5 buttons), Price opinion (Too high · About right · Good value), "What did they like?" and "What would stop them?", and "Next step" (Schedule a second showing, Write an offer, Keep looking). Saving updates the listing's showing summary and the contact's timeline; "Write an offer" moves or creates the buyer deal at Offer with this property.
- Each showing downloads as an .ics file (title "Showing · 4127 E Desert Willow Dr · Priya & Dev Raman", the address as location, workspace timezone).

### 7. Pipeline (/pipeline)
- Board like 05-pipedrive-board and 08-fub-deal-board: a 3px coloured bar on top of each column in the stage colour, columns 264px wide on #F3F1EC. Stages: Lead, Showing, Offer, Under contract, Closed, Lost (Lost collapsed to a narrow column by default). Header: stage name, count, volume ("$2.4M") and est. commission ("$47,800 GCI"). A side toggle on top: All · Buyers · Sellers.
- Card: the contact name, side chip (Buyer / Seller / Both), the property address or "Searching · Chandler, Gilbert", price ("$812,000 offer" / "$985,000 list" / "$650K-$725K budget"), "Est. $20,300 · you $16,240", agent initials, days in stage ("12d", turning red with "18d in stage" after 14 days, in Lead, Showing and Offer only; Under contract cards never show red days, and Closed and Lost cards show no day count), the next step line ("Showing · Today 10:00 am") or an amber "No next step". Under-contract cards add "Closes in 18 days" and the next open checklist item ("Appraisal due Mon"). A "…" menu on every card: Move to… (each stage), Mark closed, Mark lost (asks for a reason), Open.
- Drag and drop also works, with an accessible fallback; nothing needs dragging.
- Deal drawer (opens from a card): contact, side, property, price fields (list price, offer price, contract price, sold price), commission rate (default from Settings, 2.5%), your split (default 80%) and flat brokerage fee (default $395), with the live calculation "Gross commission $24,625 · your share $19,305", key dates, and for Under contract the deadline checklist: Offer accepted · Earnest money due (3 days after acceptance) · Inspection period ends (10 days) · Appraisal · Loan approval · Final walkthrough · Close of escrow, each with a date, a done checkbox and the due-state colour. Changing the stage to Under contract fills the checklist from the acceptance date; changing to Closed asks for the sold price and close date and turns the contact into a Past client when it's their only open deal.
- An "Edit stages" button opens the stage editor (the same editor as Settings).
- Phone: scrollable stage chips with counts at the top, then the selected stage's cards as a vertical list, each with a "Move to…" button.

### 8. Calls (/calls): speed-to-lead call mode
- Queue: new leads from listing pages and forms that nobody has called yet, newest first (red "Speed to lead · 35m" after 5 minutes); then open call tasks due today or overdue, sorted by due time. Header: "2 new leads · 6 calls due", and today's tally "Today: 5 calls · 2 talked · 1 showing booked".
- Desktop: the queue list on the left (360px; each item shows name, the context line and a reason chip "New lead · 35m" / "Overdue 2d" / "Today 3:15 pm"), the current contact on the right:
  - Name, type, temperature picker and stage picker; what they want in one line ("Showing request · 4127 E Desert Willow Dr · $985,000" or the buyer criteria); the phone number in large type.
  - A large terracotta "Call Jordan" button (52px, full card width) that opens the phone's dialler (tel:), with Text and Email beside it.
  - "Last touches": the last three activities in one line each.
  - Outcome buttons in a 4x2 grid: No answer · Left voicemail · Talked · Showing booked · Appointment set · Not ready yet · Not interested · Bad number.
  - Note: "What did they say?"
  - Next follow-up, pre-filled by the outcome and changeable in one tap (chips like 06-pipedrive-lead-detail: In 3 hours · Tomorrow 9:00 am · In 2 days · Next week · Pick a date) with a type select (Call, Text, Email, Showing, Appointment). The proposals: No answer → call again in 2 hours, then tomorrow morning after the second no-answer in a row; Left voicemail → text in 1 hour; Talked → call in 3 days; Showing booked → opens the Schedule showing dialog with the property pre-filled, and the deal moves to Showing; Appointment set → a listing or buyer-consultation appointment task at the chosen time; Not ready yet → set temperature Cold and a call in 30 days; Not interested → no follow-up, with "Mark lost" ticked; Bad number → flag the number and add a to-do "Find a new number".
  - The Calls top bar stays fixed and the footer (Skip · Press Enter to log · Log call & next) is sticky at the bottom of the right pane, so logging never needs a scroll at 1440x900.
  - "Log call & next" (Enter): saves the activity, the note and the next task in one request, shows a toast "Logged · next call Sat 9:00 am" with Undo, and slides to the next contact. "Skip" moves on without logging.
- Phone: one contact per screen with "3 of 8" and arrows at the top; a sticky full-width "Call Jordan" button at the bottom; outcome buttons 2x4 at 48px; choosing an outcome opens a bottom sheet with note, next follow-up and "Log & next".

### 9. Import (/import)
- The upload sits at the TOP of the page: a dashed drop zone, "Drop a CSV or Excel (.xlsx) file here, or Choose file", "Download a sample CSV", and the recognised columns listed (first name, last name, email, phone, type, temperature, budget min, budget max, areas, beds, pre-approved, address, source, tags, notes).
- If the file can't be parsed, say "We couldn't read that file. Make sure it's a CSV or .xlsx file with a header row." with the reason from the server, and "Choose another file".
- After upload: a mapping table (your column, two sample values, the Porchlight field it maps to). Guess the mapping from header names (including Zillow, Realtor.com and open-house sign-in sheet headers such as "Lead Name", "Phone Number", "Price Range"); the choices include "Custom field" and "Don't import".
- Duplicate check on lowercased email and normalised phone. "When a row matches a contact you already have: (•) Skip  ( ) Merge, filling empty fields  ( ) Create anyway".
- Preview of the first 20 rows, each with a status: New, "Duplicate of Priya & Dev Raman (phone)", or "Error: no phone or email". Summary: "21 new · 2 duplicates · 1 error", then "Import 21 contacts".
- Result card: "Imported 21 contacts. 2 skipped, 1 error." with View contacts and Undo.
- History table: file, imported (date and person), rows, created, updated, skipped, errors (click to see the failing rows and why), and "Undo batch" (confirm; removes the records the batch created). Below 640px each batch is a card.
- The first real import while demo data exists asks: "Remove the demo data first? It keeps reports clean." [Remove demo data and import] [Keep it].

### 10. Lead capture (/capture), the public listing pages (/l/:slug) and the home-value form (/f/home-value)
- Health first: a green dot "Last lead 35m ago · 6 this week", or an amber warning when no lead has arrived for 7 days: "No new leads for 9 days. Check your listing pages are still linked."
- Sections: Listing pages (each of our listings with its link, Copy, Open, and requests this week), the "What's my home worth?" seller form (its link, fields with Required toggles, notify emails maya@castillohomes.example, thank-you message), embed tabs ("iframe" and "HTML form") with code and Copy, and the webhook.
- Webhook: POST <origin>/api/leads with "Authorization: Bearer <workspace key>"; the key masked as "pl_••••8f2c" with Reveal and Regenerate; a sample curl whose JSON body carries first_name, last_name, email, phone, type, message, listing_address, utm_source, utm_medium and utm_campaign. GET on that route returns 405 and never lists data. Leads from it get the source "Lead webhook".
- Submissions table: received, name, form (Showing request · address / Question · address / Home value), source/medium, contact (link), status (Not called yet or Contacted).
- Public listing page /l/<slug>, no sign-in needed, like 11-listing-page in our colours: the team's initials badge ("CH", ink on #F3F1EC) with "Castillo Home Group" and "Maya Castillo · (480) 555-0100" at the top (not the Porchlight mark); a small amber line under it on demo listings: "Demo listing · photos are AI-generated"; the big photo plus 2 more; the status dot ("For sale" / "Coming soon" / "Pending"), the price, "4 bd · 3 ba · 2,840 sq ft", the address; "About this home" (description), a facts grid (year built, lot, garage, pool, HOA, days on market); and a sticky right card (a bottom sheet on the phone) with a primary "Request a showing" and a secondary "Ask a question", plus the agent's phone as a tel: link.
  - Request a showing: day chips for the next 7 days, then time chips (9:00 am to 6:00 pm every 30 minutes, hiding times that clash with confirmed showings of this listing), name, email, phone, "Are you working with an agent?" (Yes / No), "Pre-approved?" (Yes / In progress / Not yet), and a message. Button "Request showing". Thank-you: "Thanks, Jordan. Maya will text you within 15 minutes to confirm Sat 10:30 am."
  - Ask a question: name, email or phone, the question. Thank-you: "Thanks. Maya will get back to you shortly."
  - It reads utm_source, utm_medium and utm_campaign from the page URL into hidden fields, and has a honeypot field and a simple rate limit.
- The home-value form /f/home-value: "What's your home worth?", the address, beds, baths, "When are you thinking of selling?" (0-3 months, 3-6, 6-12, Just curious), name, email, phone; thank-you "Thanks. Maya will send your home value report within one business day."
- Every submission is SAVED FIRST (contact with type and source "Listing page: <address>" or "Home value form", the form activity, a Requested showing for showing requests, and a "Call back" task due in 5 minutes); only then does it notify: an email to the notify list if email is connected, the outbound webhooks, and "New leads" on Today. The thank-you always uses the first name typed in the form, never a stored contact's name. A submission is matched to an existing contact only when the email matches (lowercased); when only the phone matches and the name differs, it creates a new contact with a note visible only inside the CRM, "Same phone as Hugh & Renee Lambert. Merge?", and a Merge button that moves the activities, showings, tasks and submissions onto the older contact. A submission from an existing contact still shows in Today → New leads and at the top of the Calls queue until someone calls back. Generate a fresh workspace key automatically when the workspace is claimed and when the owner runs Remove demo data, so a real install never keeps the template's key.

### 11. Reports (/reports)
- Filters: This year / Last 12 months / Last 90 days ("12 mo / 90 days" on the phone), Agent (All, Maya, Ben). A note: "Demo data included", or excluded once real records exist; demo and real records are never mixed.
- Six cards in a 2-column grid, charts in muted stage colours on white, numbers tabular:
  - Commission: closed GCI and your share this year, pending (under contract), and projected (offers at 50%) as three numbers, plus closed GCI by month (bars, last 12 months).
  - Pipeline by stage (horizontal bars; volume and count; "12 leads aren't in the pipeline yet" as a text line).
  - Lead sources to closings (table: source → contacts → showing → under contract → closed, with %): Zillow, Listing page, Home value form, Referral, Sphere, Open house, Instagram.
  - Showings per week (last 8 weeks, stacked by Done and Cancelled) with average interest.
  - Speed to lead (median minutes from a form lead to the first call, per source, and the 5 slowest leads).
  - Listings: days on market and showings per active listing, with the average.

### 12. Settings (/settings), owner only
Left sub-nav:
- Business: team name, agent name, phone and brokerage line ("Brokered by Desert Crest Realty"; shown on listing pages), currency (Intl formatting), timezone, date format, 12 or 24-hour time, commission defaults (rate 2.5%, your split 80%, flat fee $395), words (rename Contact, Deal, Showing, singular and plural). All UI text lives in one strings file, so a remix can translate it.
- Stages: rows with name, colour swatch (the muted palette), kind (Open, Closed or Lost), deal count, up/down buttons (and drag), "+ Add stage", and delete, which asks where to move its deals. The board updates at once.
- Team: members (name, email, role Owner or Member with the job title, e.g. "Owner · Lead agent", "Member · Buyer's agent", visibility "All contacts" or "Only their own contacts", last active) and "Invite teammate", which creates a /join link to copy (also emailed if email is connected).
- Templates: 5 email templates with merge fields ({{first_name}}, {{last_name}}, {{listing_address}}, {{listing_price}}, {{showing_time}}, {{agent_name}}, {{agent_phone}}; fallback written as {{first_name|there}}) and a live preview for a chosen contact with missing fields highlighted red; and 3 quick texts for Text buttons ("Hi {{first_name}}, it's {{agent_name}} with Castillo Home Group. Got your request for {{listing_address}}. I can show it {{showing_time}}. Does that work?").
- Integrations: cards, each with an honest status pill (Connected / Not connected / Error) and one line on what it does:
  - Email (Resend): Not connected. "Send from your own domain." Connect = paste a Resend API key (stored server-side, shown only as "re_••••3f2a") and the From address; then "Send a test email" to yourself, and the bounce webhook URL. CRM email is sent only with the owner's OWN key, never with the platform's built-in RESEND_API_KEY. Without it, one-to-one email is "Open in your email app" (a mailto: with subject and body filled in) and logs "Sent from your email app". Provider error text never stores or shows an email address: replace any address with "[email]".
  - Phone and text links: Connected. "Opens your phone's dialler or Messages with the text filled in. Nothing to set up."
  - Listing pages and lead webhook: Connected, "9 listing pages · last lead 35m ago".
  - Outbound webhooks (Zapier, Make, n8n): status from the endpoints.
  - Calendar files (.ics): Connected.
  - Not connected, each with a line on what it would do: MLS / IDX feed, ShowingTime, Zillow Premier Agent, Realtor.com leads, Dotloop, DocuSign, Google Calendar, Twilio SMS, Gmail or Outlook sync.
- Webhooks: endpoints (URL, events contact.created, showing.requested and deal.stage_changed, an auto-generated secret with Copy), "Send test event", and a delivery log (time, event, status code). Each delivery is signed: header X-Porchlight-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">; up to 3 retries.
- Data: "Export all data" (a .zip of CSVs), "Refresh demo dates" (while demo data exists), "Reset demo data" (puts every demo record back exactly as on first boot with dates relative to today, touching only demo records; confirm "Reset the demo to its original state? Your own records stay."), and a danger zone "Remove demo data": a dialog listing exactly what goes (counts of demo contacts, listings, showings, deals, tasks, activities, imports and form submissions; "Your own records stay."), where typing REMOVE confirms.

## Built-in rules
- Every stage change writes an activity and fires deal.stage_changed.
- A call outcome proposes the next task (rules above). A completed showing creates a "Get feedback" task if no feedback was saved.
- A deal moved to Under contract builds its deadline checklist; a checklist item due within 2 days appears in Today's follow-ups.
- Do-not-contact hides the Call/Text/Email buttons behind a "Do not contact" label and blocks every email.
- Phone links: tel:+14805550162; sms:+14805550162?&body=<quick text>, with the text URL-encoded. Directions: https://www.google.com/maps/dir/?api=1&destination=<url-encoded address>.
- Members whose visibility is "own" only ever receive their own records from the API: enforce it on the server. Settings, demo removal, export, team, integrations, webhooks and stages are owner-only.
- Every /api route needs a session except auth, the public listing page data, the public form POSTs, POST /api/leads (API key) and the Resend webhook (signature). The public listing API returns only listing fields, photos and free time slots, never contacts or showings.

## Data model (Prisma, SQLite)
workspace (name, agentName, agentPhone, brokerageLine, currency, timezone, dateFormat, timeFormat, locale, words JSON, commissionRateBps, agentSplitBps, flatFeeCents, claimedAt, demoSeededOn) · membership (userId, role owner|member, title, visibility all|own, isDemo) · invite · contact (firstName, lastName, partnerName, displayName, type buyer|seller|both|past_client|sphere, temperature hot|warm|cold, email, phone, altPhone, mailingAddress, source, utm JSON, ownerId, doNotContact, emailBounced, birthday, homeAnniversary, tags, custom JSON, importBatchId) · buyerCriteria (contactId, budgetMinCents, budgetMaxCents, areas JSON, minBeds, minBaths, mustHaves JSON, timeline, preapprovalStatus, lender, preapprovalCents, preapprovalExpiresOn) · sellerInfo (contactId, propertyAddress, estValueCents, motivation, timeline, listingApptAt) · listing (slug, ours boolean, brokerage, address, city, zip, priceCents, beds, baths, sqft, lotSqft, yearBuilt, garage, pool, hoaCents, mlsNumber, status, listedOn, goesLiveOn, description, features JSON, photos JSON, sellerContactId, commissionRateBps) · priceChange (listingId, fromCents, toCents, changedAt) · openHouse (listingId, startsAt, endsAt) · showing (listingId, contactId, agentId, tourId, startsAt, endsAt, status requested|confirmed|done|cancelled, cancelReason, source, notes) · showingFeedback (showingId, interest, priceOpinion, liked, concerns, nextStep) · pipeline and stage (name, position, kind open|closed|lost, colour) · deal (contactId, listingId, side buyer|seller, title, listPriceCents, offerPriceCents, contractPriceCents, soldPriceCents, commissionRateBps, agentSplitBps, flatFeeCents, stageId, ownerId, acceptedOn, expectedCloseOn, closedOn, lostReason, stageEnteredAt) · dealMilestone (dealId, kind, dueOn, doneAt) · task (kind call|text|email|showing|appointment|todo, title, dueAt, assigneeId, contactId, dealId, listingId, doneAt, snoozedUntil) · activity (type call|text|email|note|showing|stage_change|form|import|price_change, outcome, body, durationSec, occurredAt, userId, contactId, dealId, listingId, showingId, emailMessageId) · emailTemplate · quickText · emailMessage (to, subject, body, status, providerId, error, templateId) · tag and tagging · savedList (name, filters JSON) · importBatch (fileName, mapping JSON, rowCount, created, updated, duplicatesSkipped, errors, errorRows JSON, undoneAt) · leadForm (slug, kind showing|question|home_value, listingId, fields JSON, notifyEmails, thankYou) · formSubmission (formId, listingId, payload JSON, utmSource, utmMedium, utmCampaign, contactId) · webhookEndpoint (url, events, secret) · webhookDelivery · integration (provider, status, settings JSON, lastCheckedAt) · apiKey · file (path, mime, size).

## Demo data: "Castillo Home Group" (all fictional, every record isDemo)
Seed it automatically at server start whenever the database has no workspace, and finish the seed before the server answers requests, so a fresh copy boots straight into the demo. Every date is relative to the moment of seeding. Store the seed day (workspace.demoSeededOn). At server start, before the server answers, and then once an hour: when whole calendar days have passed since the seed day in the workspace timezone, move every isDemo timestamp and date forward by that many days (created, updated, due, starts, ends, occurred, listed, goes-live, accepted, expected close, closed, milestone due, price change, open house, pre-approval expiry, stage entered, sent) and save the new day, so "today", "this week", "closes in 18 days" and every dated view look exactly as on seed day, and times of day hold. Rows a user created are never shifted. Settings → "Refresh demo dates" does the same on demand. Keep weekday names out of seeded note text: the chips show the real days. USD, America/Phoenix.
- Users: Maya Castillo, owner and lead agent (maya@castillohomes.example), and Ben Okafor, buyer's agent, a member who sees only his own contacts (ben@castillohomes.example). Both use password porchlight-demo. Ben owns the buyers Marcus Bell, Grace Whitman, Kevin Osei and 9 other buyer contacts; Maya owns the rest.
- Phones (480) 555-0100 to 0199 and (602) 555-0100 to 0149; emails end in .example; Maya's public number is (480) 555-0100.
- Business settings: "Brokered by Desert Crest Realty", commission 2.5%, split 80%, flat fee $395.
- Our 9 listings (slug · address · city · price · bd/ba/sq ft · status · seller · days on market · showings):
  1. 4127-e-desert-willow-dr · 4127 E Desert Willow Dr · Scottsdale 85255 · $985,000 (reduced from $1,000,000 6 days ago) · 4/3/2,840, pool, 3-car garage, built 2004, no HOA · Active · Carol & Jim Hendricks · 12 days · 9 showings (3 this week), avg interest 3.6, top comment "price feels high for the backyard"
  2. 1840-n-44th-pl · 1840 N 44th Pl · Phoenix (Arcadia) 85018 · $1,450,000 · 4/4/3,120, pool, built 1958 remodeled · Coming soon, goes live in 4 days · Nadia Farouk
  3. 762-s-maple-ridge · 762 S Maple Ridge · Mesa 85208 · $529,000 · 3/2/1,910, single story, HOA $85/mo · Active · The Brennan family (Mike & Dana Brennan) · 21 days · 11 showings · an offer received at $515,000, counter due tomorrow 5:00 pm
  4. 2210-e-cactus-wren-ct · 2210 E Cactus Wren Ct · Chandler 85286 · $575,000 · 4/2.5/2,260, pool · Active · Ines Moreau · 8 days · 5 showings
  5. 3305-e-ocotillo-rd · 3305 E Ocotillo Rd · Gilbert 85298 · $634,000 · 4/3/2,480 · Pending (under contract, closes in 11 days) · Sam Whitfield
  6. 905-w-palo-verde-dr · 905 W Palo Verde Dr · Tempe 85282 · sold $472,000 (listed $479,000) · 3/2/1,640 · Sold 9 days ago · Linda Cho
  7. 618-e-lemon-st · 618 E Lemon St · Tempe 85281 · $419,000 · 2/2/1,180 townhome, HOA $210/mo · Active · Paul Ferreira · 30 days · 14 showings, a price reduction suggested in a note
  8. 9832-n-115th-way · 9832 N 115th Way · Scottsdale 85259 · $1,195,000 · 5/4/3,560, pool, mountain views · Coming soon, goes live in 9 days · Hugh & Renee Lambert
  9. 1977-e-fountain-st · 1977 E Fountain St · Mesa 85203 · no price yet · 3/2/1,720 · not listed: the Delgado listing appointment is today at 5:30 pm (create the listing record as Coming soon without a price, hidden from the public until a price is set)
- 7 other-brokerage properties (ours = false, brokerage names fictional: "Sonoran Peak Realty", "Valley Gate Homes"): 2871 S Rosemary Dr, Chandler, $689,000, 4/3/2,610 · 1458 E Mesquite St, Gilbert, $705,000, 4/3/2,700 · 2418 E Saguaro Ln, Scottsdale, $825,000, 4/3/2,950 · 615 S Ash Ave #12, Tempe, $389,000, 2/2/1,050 condo · 1129 W Juniper Ave, Gilbert, $552,000, 4/2.5/2,300 · 2045 S Mill Ave #204, Tempe, $415,000, 2/2/1,120 · 3871 E Vaughn Ave, Gilbert, $465,000, 3/2/1,780.
- Key contacts and deals (est. commission = price x 2.5%; your share = 80% minus $395):
  - Priya & Dev Raman · buyers · Hot · Zillow · budget $650K-$725K · Chandler, Gilbert · 4+ bd, 3+ ba · Pool, No HOA · pre-approved $700,000 with Desert Sky Lending (fictional), expires in 41 days · stage Showing, 9 days in stage · today's tour: 10:00 am 2871 S Rosemary Dr and 10:45 am 1458 E Mesquite St (Confirmed), toured 4127 E Desert Willow Dr 5 days ago (interest 3, "Loved the kitchen, backyard too small"). Priya's mobile (480) 555-0162.
  - Hannah & Luke Pruitt · buyers · Hot · Referral · $800K-$850K · Scottsdale · stage Offer: offer $812,000 on 2418 E Saguaro Ln, submitted yesterday, the seller's response is due today 6:00 pm (a due-today task "Call listing agent about the Saguaro offer · 4:00 pm").
  - Jordan Pike · buyer · New lead from the listing page 35 minutes ago: "Showing request · 4127 E Desert Willow Dr", wants today 1:30 pm (a Requested showing today 1:30 pm), not working with an agent, pre-approval in progress, source "Listing page: 4127 E Desert Willow Dr" with utm_source=instagram, utm_medium=social, utm_campaign=desert-willow-reel. Not called yet (so it's red "Speed to lead" on Today).
  - Alicia Gomez · buyer · New lead 3 hours ago: "Question · 618 E Lemon St: Is the HOA fee monthly and does it include water?" · not called yet.
  - Marcus Bell · buyer (Ben) · Warm · Open house · first-time buyer $380K-$420K · Tempe, Mesa · 2+ bd · pre-approval In progress (overdue task: "Call Marcus about the pre-approval letter", 2 days) · showing today 4:00 pm at 615 S Ash Ave #12 (Confirmed, agent Ben).
  - Grace Whitman · buyer (Ben) · Warm · Relocation referral · moving from Chicago · $1.1M-$1.3M · Scottsdale · 4+ bd · Single story, Pool, 3-car garage · pre-approved $1,250,000, expires in 6 days (shows red "Expires in 6 days") · overdue email "Send Grace the north Scottsdale shortlist" (1 day) · matches 9832 N 115th Way.
  - Tomás & Elena Ruiz · buyers · Under contract on 1129 W Juniper Ave at $548,000, accepted 12 days ago, closes in 18 days; checklist: earnest money done, inspection done, appraisal due in 3 days, loan approval due in 10 days, final walkthrough the day before closing.
  - Kevin Osei · buyer (Ben) · Under contract on 2045 S Mill Ave #204, Tempe, a $415,000 condo (another other-brokerage property, 2/2/1,120), closes in 6 days; final walkthrough in 5 days; loan approval done.
  - Sam Whitfield · seller · Under contract at $628,000 on 3305 E Ocotillo Rd, accepted 19 days ago, closes in 11 days; inspection response done; appraisal done; loan approval due in 4 days.
  - Carol & Jim Hendricks · sellers · 4127 E Desert Willow Dr · stage Showing · next step "Weekly showing report call · Sat 10:00 am".
  - The Brennan family · sellers · 762 S Maple Ridge · stage Offer: offer $515,000 received, counter due tomorrow 5:00 pm (task "Send Brennans the counter-offer options", tomorrow 11:00 am).
  - Nadia Farouk · seller · 1840 N 44th Pl · stage Lead (listing signed, Coming soon) · task "Confirm photographer for 1840 N 44th Pl" in 2 days.
  - Robert & Ann Delgado · sellers · Home value form 2 days ago (utm_source=google, utm_medium=cpc, utm_campaign=home-value-mesa) · listing appointment today 5:30 pm at 1977 E Fountain St (an appointment task) · overdue "Send the Delgados the CMA before the appointment" (1 day).
  - Ines Moreau · buyer & seller · selling 2210 E Cactus Wren Ct (Showing) and buying in Gilbert (Lead) · overdue call "Talk to Ines about a price reduction" (3 days).
  - Linda Cho · seller · Closed 9 days ago at $472,000 · now a Past client.
  - Ashley Navarro · buyer · Closed 4 days ago at $462,000 on 3871 E Vaughn Ave, Gilbert (other brokerage) · now a Past client.
  - Derek Lindqvist · buyer · Lost 20 days ago, "Bought with another agent".
  - Frank Mertz · seller · Lost 35 days ago, "Listed with a discount brokerage", do not contact.
  - Paul Ferreira · seller · 618 E Lemon St · stage Showing, 30 days in stage (red) · NO next step.
  - Hugh & Renee Lambert · sellers · 9832 N 115th Way · stage Lead (listing signed, coming soon).
  - Fill to 56 contacts with: 8 more buyers (mixed Hot/Warm/Cold, budgets $350K-$900K, areas across Tempe, Mesa, Chandler, Gilbert, Phoenix, Scottsdale; 3 of them Ben's), 10 past clients with closed deals spread over the last 11 months (prices $395K-$890K, sides mixed, birthdays and home anniversaries set; 2 anniversaries fall in the next 7 days and show as "Home anniversary" follow-ups), and 8 sphere contacts (friends, a lender "Desert Sky Lending · Rachel Kim", a title officer "Copper State Title · Omar Haddad", an inspector, a stager) with no deal. Common US names; every one fictional.
- That makes deals: Lead 4, Showing 7, Offer 2, Under contract 3, Closed 12 (2 this month: Linda Cho $472,000 and Ashley Navarro $462,000 → closed this month GCI $23,350), Lost 2; plus about 12 contacts in Lead with no deal yet. Days in stage spread from 1 to 30.
- Showings: about 60 over the last 8 weeks and the next 7 days, most Done with feedback, a few Cancelled. Today: 10:00 am and 10:45 am Raman tour (Confirmed), 1:30 pm Jordan Pike at 4127 E Desert Willow Dr (Requested), 4:00 pm Marcus Bell at 615 S Ash Ave #12 (Confirmed, Ben). This week also: Grace Whitman tour of 3 homes in 2 days (Ben), an open house at 4127 E Desert Willow Dr on Sunday 11:00 am-2:00 pm, and 4 more showings spread over the week.
- Activities: about 240 over 60 days: calls with outcomes, texts, emails (all Delivered except 1 Bounced, "Mailbox does not exist"), notes, showings, stage changes, form submissions, price changes and the import.
- Tasks: about 26 open and 45 completed. Overdue (4): Ines Moreau call (3 days), Marcus Bell call (2 days), Grace Whitman email (1 day), Delgado CMA (1 day). Due today (5 besides showings): 9:00 am text Priya the tour addresses; 11:00 am call Alicia Gomez back; 2:00 pm "Order appraisal update · Ruiz · 1129 W Juniper Ave"; 4:00 pm call listing agent about the Saguaro offer (Pruitt); 5:30 pm Delgado listing appointment. Next 7 days (about 14): the Brennan counter, the Hendricks weekly call, Nadia's photographer, the Ruiz appraisal, the Osei final walkthrough, the Whitfield loan approval, Grace's tour, 2 home anniversaries, feedback follow-ups and first calls to two cold buyers.
- 5 email templates: "New buyer lead reply" ("Hi {{first_name|there}}, thanks for asking about {{listing_address}}. I can show it {{showing_time}}. Are you pre-approved yet? Happy to connect you with a lender. {{agent_name}}, {{agent_phone}}"), "Showing confirmation", "Feedback after a showing", "Price update for sellers" (weekly showing report), and "Home value follow-up" (CMA attached by hand).
- 3 quick texts: the showing reply above, "On my way, see you at {{listing_address}} in 10 minutes.", and "Thanks for touring {{listing_address}} today. What did you think?".
- 1 import batch: sphere-contacts.csv, 30 days ago, by Maya: 18 rows, 16 created, 1 duplicate skipped (Rachel Kim by email), 1 error (row 12, "Uncle Ray": "No phone or email").
- Lead forms: one "Request a showing" and one "Ask a question" form per our listing (shared fields), and the home-value form. 6 submissions: Jordan Pike (35m ago), Alicia Gomez (3h ago), Delgado (2 days ago, home value), and 3 older ones linked to contacts who have since been called.
- Saved lists: Hot buyers, Pre-approval expiring, Past clients anniversaries this month, plus the built-in lists.
- Integrations: Email (Resend) Not connected; phone and text links, listing pages and .ics Connected; no outbound webhooks yet (an empty state with one "Add endpoint" button).

## States
- Loading: skeleton rows and cards shaped like the real ones, never a blank page or a spinner in the middle of nowhere.
- Errors: a plain-English card ("We couldn't load your contacts. Check your connection and try again.") with one "Try again" button; failed saves keep what was typed and show a toast with Retry. Never show a load failure as empty or as zero.
- Every empty state offers exactly one next action: no follow-ups → "Import your contacts"; no contacts in a list → "Clear filters"; no deals in a stage → "Move a contact here from Contacts"; no showings this week → "Schedule a showing"; no listings → "Add your first listing"; no submissions → "Copy a listing page link".

## Build order
Work in small files and get a first version running before polishing:
1. Prisma schema, the seed and the demo-date refresh, Better Auth with the demo users and the auth-secret-at-boot, then the API routes in separate files (today, contacts, listings, showings, deals, tasks, activities, calls, imports, forms and public, reports, settings, webhooks, export). Test each with curl.
2. Generate the listing photos with the script (in the background while you build the UI).
3. Theme tokens, app shell (sidebar, bottom bar, demo banner, Cmd-K) and the sign-in pages.
4. Today, then Contacts, Contact detail, Listings and listing detail, Showings with the scheduler, Pipeline with the deal drawer, Calls, the public listing page and home-value form, Lead capture, Import, Reports, Settings.
5. Phone layouts at 390px, every loading, empty and error state, dark mode, favicon, OG image and title.

Build it fully, run it, and fix every error. Deliver the complete app, not a plan. When it's done, delete refs/ and any other reference screenshots from the workspace (keep webapp/public/listings, favicon.svg and og.png), reset the demo, delete every Session and Verification row, and run `PRAGMA wal_checkpoint(TRUNCATE); VACUUM;` on the committed database.
All templates