# Lovable build spec: deal-room workspace

The complete spec for Path 2, the one we built our own workspace from.

**How to use it.** Export this tab as Markdown (or copy it into a .md file). Open a new Lovable project, switch to plan mode, attach the file plus your brand folder, and send:

```text
Attached is the full build spec for the app I want. Read it end to end, ask me for the brand inputs
listed in section 0.2, then produce a phased plan and build it step by step. Follow the spec's
decisions exactly unless I say otherwise.
```

The agent then drives the build. You supply brand values, assets and approvals when asked. Replace anything in `` with your own values.

## Agent instructions (read first)

You are building the app described below for an operator who may have never used Lovable. Rules for this build:

1. **Ask before assuming.** Collect the inputs in section 0.2 (brand palette, fonts, logo, sample assets, first customer) before writing feature code. Do not invent brand values.
2. **Build in the order given.** Foundation, registries, data, server layer, deal rooms, sharing and tracking, agent tooling. Do not jump ahead to deal rooms before the design system and registries exist.
3. **No end-user auth, no CRUD admin forms.** The Lovable editor is the admin UI. Operator actions are server functions the operator triggers by prompting.
4. **Checkpoint after each phase.** Show what was built, how to verify it, and wait for approval before the next phase.
5. **Every prompt block below is a specification**, not literal user copy. Implement its intent with this project's stack and conventions.
6. **Record the durable rules** (brand tokens, thumbnail rules, mobile rules, copy rules) into project memory, and create the deal-room skill described in Part 7.

## Part 0: Before you prompt

### 0.1 The one mental-model shift

Most apps have an admin UI: forms, buttons, an editor screen for the person who runs the business. **This app does not.** The Lovable editor is the admin UI.

- There is **no end-user login** and **no CRUD forms**. Nobody signs up.
- You, the operator, create content by talking to the agent in chat: "duplicate the Acme deck for Globex, change the pricing to X".
- The only people who see a URL are **prospects**, and they only ever see a deal room or a share link. They never see the workspace.

The effort goes into **registries, renderers, share plumbing and agent tooling** instead of forms and auth. If you find yourself asking for a "create deal room form", stop. Ask for a server function and an MCP tool instead.

### 0.2 What you need ready before step 1

| What | Why | Format |
| --- | --- | --- |
| Your logo | Header, deal-room co-branding | PNG, white on transparent preferred |
| Brand palette | Design tokens | 1 accent hex, 1 near-black, 1 paper/off-white |
| Two typefaces | Display + body | Google Fonts names, or font files |
| 3 real assets | Something to build against | 1 slide deck (PDF or screenshots), 1 PDF, 1 Loom/YouTube URL |
| 1 customer name + website | Your first deal room | e.g. "Globex, globex.com" |
| A headshot | The "welcome from \" block | JPG/PNG |

If you build the design system against placeholder brand values you will redo every asset later.

### 0.3 Lovable mechanics that prevent early mistakes

1. **Plan mode vs build mode.** For anything bigger than a copy tweak, start in plan mode. Read the plan, correct it, then approve.
2. **Preview is not live.** Frontend changes require a manual Publish/Update. Backend and database changes are live immediately. A customer seeing a 404 or old content almost always means you forgot to publish.
3. **Attach files, do not describe them.** Typing numbers by hand introduces typos into customer-facing documents.
4. **Memory is a rule engine.** "Remember this" writes a persistent rule for every future session. Use it for brand, thumbnail, mobile and copy rules, not one-off facts.
5. **Skills are checklists.** Build a "create a deal room" skill once and every future room comes out consistent.
6. **Credits.** Each message costs credits. Batch related changes into one message.
7. **Verify in incognito.** Anything customer-facing gets checked in a private window at phone width before it is sent.

### 0.4 The shape of what you are building

```text
WORKSPACE (private, editor-operated)
  /                       library home: deal rooms, decks, one-pagers, templates, apps, uploads
  /design-system          tokens, primitives, motion, asset gallery
  /decks/           deck viewer, /present, /print, /links
  /        standalone one-pager routes
  /templates//      parameterized instance renderer
  /apps/            embedded HTML apps and native app routes
  /dealrooms              deal-room manager + analytics

PUBLIC (what prospects see)
  /room/            the deal room
  /room//a/   a single asset, embedded, no way back into the workspace
  /s/              a tokenized share link to any single asset

AGENT SURFACE
  MCP server              list_assets, add_asset_link, attach_assets_to_dealroom, registry listers
  skill                   create-dealroom checklist
  memory                  brand, thumbnail, mobile, copy rules
```

## Part 1: Foundation

### Step 1. Create the project and turn on the backend

```text
Create a new project called  Workspace. This is an internal sales-asset workspace operated
entirely from the Lovable editor. There is no end-user authentication and no public sign-up: I am the
only operator, and I will create all content by prompting you. The only public surfaces will be
customer-facing deal rooms and tokenized share links.

Please enable Lovable Cloud now, because I will need a database, file storage, and server functions.
Do not add any login page, auth route, or user table. Confirm which framework this project uses and
show me the folder layout before writing any feature code.
```

**Verify:** Lovable Cloud is enabled; there is no /auth or /login route. **If it goes wrong:** "Remove the auth pages and user tables. This app has no end-user auth. Operator actions run through server functions that I invoke by prompting you."

### Step 2. Design system first

The highest-leverage step. Every deck, one-pager, template and room inherits from it.

```text
Build the design system foundation before any feature work.

Create src/styles/.css holding all visual tokens as CSS custom properties. Nothing anywhere
else in the app may hardcode a color, shadow, radius, duration, or easing value.

Tokens:
- Color: --ink: , --paper: , --accent: , plus a muted/faint/ghost/
  hairline ladder derived from paper at 0.62, 0.40, 0.22 and 0.12 alpha, and two elevated surfaces.
- Type: --font-display: , --font-body: . Load them with a  tag in the
  root route, never with an @import inside the CSS file.
- A fluid type scale using clamp() for display, headline, title, body and caption, with utility classes.
- Spacing scale, radii, and two shadow levels.
- Motion: --dur-fast/base/slow and brand ease-out / ease-in curves, plus a linear easing.

Default aesthetic: dark editorial theme, near-black background, off-white text, the accent used
sparingly for emphasis and never as a large fill. No purple or indigo gradients, no default system fonts.

Then create a /design-system route with sections Tokens, Primitives, Templates, Assets, Motion, Slides.
Populate Tokens with live swatches and type specimens from the actual CSS variables; leave the others
as labelled shells. Add the attached logo and use it in the design-system header.
```

**Verify:** /design-system/tokens renders, and changing one variable changes them all. **If it goes wrong:** "Replace every hardcoded color in the components you just wrote with the corresponding token."

### Step 3. The workspace shell

```text
Build the workspace home at / as a dark, dense, editorial library screen. It is the operator's
cockpit, not a marketing page.

Top to bottom: a slim header (logo, workspace name, links to Deal rooms, Assets, Design system);
sections for Deal rooms (cards with customer name, status pill draft/live/archived, last updated,
later visitor counts), Decks, One-pagers, Templates, Apps and Uploads, each a grid of cards.

Rules:
- Every card links to the item's viewer, plus a small "Share links" action where sharing applies.
- Section headings show a count.
- Hydration-safe rendering: never conditionally render an element based on data that differs between
  server and client. Render it and disable or dim it instead.
- Cards mount heavy previews only when scrolled into view.
- Grids collapse to one column at 640px and under.
```

**Verify:** / renders with empty sections and no console or hydration warnings.

## Part 2: Asset types and their registries

Every asset type has a **registry**, a TypeScript array or table that the home screen, the room item picker and the agent's tools all read. If an asset is not in a registry, the agent cannot find it and you cannot attach it to a room.

### Step 4. Decks

```text
Build the deck system. A deck is a folder of slide components, not a database record, so I can edit
slides by prompting you.

- src/decks/types.ts: SlideMeta (slug, title, tags, maxSteps, declarative animations array) and Deck.
- src/decks//slides/NN-.tsx: one file per slide, numeric prefix sets order.
- src/decks//index.ts: glob the slides, sort by filename, export the Deck.
- src/decks/_registry.ts: every deck, loaded lazily.
- src/decks/_scaffold.ts: helpers that emit canonical slide and deck files; the only supported way to
  create a slide.

Components: a 16:9 slide frame with slide number, a ScaledSlide that scales a fixed-size slide to any
container (so slides look right on phones), header and portrait primitives, and a step-animation hook.

Routes: /decks/$slug viewer with keyboard arrows, click zones and touch swipe; /present full screen;
/print paginated; /links for share links.

Build one real deck from the attached content to prove it end to end.
```

**Verify:** arrows and swipe work, /print produces clean pages.

### Step 5. One-pagers

```text
Add the one-pager system: standalone long-form routes for overviews, proposals and executive briefs.

- src/onepagers/_registry.ts exports { slug, title, description, tags, route }[].
- Two flavours: fluid (max-width + responsive type) and fixed-width print page (US Letter or A4).
- Fixed-width one-pagers register their natural width so the room viewer can scale them to a phone
  instead of clipping.
- Every one-pager uses a useEmbedded() hook: true inside an iframe or with ?embed=1, and then the page
  hides its "back to workspace" toolbar, so a prospect cannot walk into the private library.
- A print stylesheet for clean PDF export.
- A short src/onepagers/AGENTS.md documenting fluid vs fixed and the mobile checklist.

Build one real one-pager from the attached content.
```

**Verify:** renders, prints cleanly, hides its toolbar with ?embed=1.

### Step 6. Templates (parameterized assets)

Templates turn "write a new file each time" into "fill in a schema". Use them for anything you produce repeatedly, most obviously proposals and briefs.

```text
Add the template system: reusable, parameterized renderers whose data lives in the database.

- A Template type with slug, title, description, kind, version, a Zod schema, a manifest describing
  fields for a human or an agent, a renderer, and defaults.
- src/templates// with schema, manifest, renderer, defaults and samples; a registry.
- Routes: /templates, /templates/$templateSlug, /templates/$templateSlug/$instanceSlug, plus /print
  and /links.
- Instances in a template_instances table: payload JSONB, payload hash, renderer version, so a stored
  instance always renders as it did when created.
- src/templates/AGENTS.md with the mobile rules for every renderer: no hardcoded page widths unless it
  is a print page, clamp() type, grids to one column at 640px, wide tables in a scroll container,
  16px side padding on mobile, nothing with min-width above 320px.

Build one pricing-proposal template with a line-item table, a totals row and a highlighted pilot box.
```

**Verify:** an instance renders correctly at 402px wide.

### Step 7. Apps, uploads and links

```text
Add three more asset kinds.

Apps: a registry of { slug, title, description, tags, route, htmlPath?, thumbnailUrl?, nativeRoute? }.
Static HTML from public/apps/ in an iframe, or a native route. Routes /apps/$slug and /apps/$slug/links.

Uploads: a private storage bucket -uploads and a server function that registers an upload row
(file name, mime type, size, storage path, title, description). Uploads are library-wide. PDFs render
inside the app, not as a download: a client-only PdfViewer, page by page, pinch and scroll friendly.

External links: an asset_links table (url, title, description, favicon, thumbnail) and a server function
that saves a URL and fetches its title. This is how Loom, Notion, Figma and docs pages enter the library.

Then build /assets: one library view of every asset kind with thumbnail, kind badge and share action.
```

**Verify:** upload a PDF, see it in /assets, open it rendered in the app at phone width.

## Part 3: Data model and security

### Step 8. One migration, locked down

The step where first-time builders create real security holes. The rule is absolute: **no table has any policy for anonymous or authenticated users. Everything public goes through a SECURITY DEFINER function that returns only safe columns.** With no end-user auth, a "public read" policy on a rooms table would expose every draft room, private note and unpublished price.

```text
Create the full schema in one migration. Every table gets explicit GRANTs, RLS enabled, and a
restrictive deny-all policy for anon and authenticated. Operator access goes through server functions
using the service role; public access goes through SECURITY DEFINER functions.

Tables:
- dealrooms: slug (unique), customer_name, title, subtitle, welcome_md, context_md, accent_color,
  cover_image_url, status (draft/live/archived), rep_note, rep_name, rep_role, rep_avatar_url,
  process_steps jsonb, timestamps, published_at.
- dealroom_items: dealroom_id, sort_order, stage, kind (deck/onepager/instance/upload/link/note/app),
  ref columns per kind, note_md, display_title, display_description, display_thumbnail_url,
  ai_personalized, ai_source_context, added_at.
- dealroom_uploads, asset_links, template_instances.
- decks and slides_index: a synced mirror of the code-defined deck registry.
- share_links: token (unique), one nullable ref column per asset kind, recipient_name,
  recipient_email, note, require_email, created_at, revoked_at.
- share_sessions, share_events, share_rate_limit, dealroom_sessions, dealroom_events.

SECURITY DEFINER functions, each with SET search_path = public, explicit column lists, and rooms
filtered to status = 'live':
- get_public_dealroom(_slug): presentation columns only, never context_md or internal notes.
- get_public_dealroom_items(_slug): items with display fields, ordered by sort_order.
- resolve_public_dealroom_item(_slug, _item_id): the single-item deep link.
- get_public_dealroom_upload(_slug, _item_id): storage_path, mime_type, file_name only.
- get_public_dealroom_instance(_slug, _item_id) and get_template_instance_public(_id).
- get_share_link_by_token(_token).
- check_share_rate_limit(_key, _max, _window_seconds): fixed-window counter with row locking.

Add updated_at triggers. Use validation triggers rather than CHECK constraints for anything
time-dependent.
```

**Verify:** the security linter shows no publicly readable tables, and an anonymous read of dealrooms returns a permission error. **If it goes wrong:** "Remove that policy. No table is directly readable by anon or authenticated. Add a SECURITY DEFINER function returning only the safe columns instead." Then lock it in: "Remember this permanently: RLS deny-all on every table, public reads only through SECURITY DEFINER functions that filter to live rows and return safe columns. Never expose internal note or context fields publicly."

## Part 4: The server layer

### Step 9. Server functions and tracking endpoints

```text
Build the server layer using this project's server-function convention, and file-based server routes
only for raw HTTP endpoints.

Conventions:
- Operator functions in src/lib/.functions.ts, safe to import anywhere.
- The service-role client is imported inside each handler (dynamic import), never at module scope.
- Every function validates input with Zod. Read process.env only inside handlers.

Room functions: createDealroom, updateDealroom, setDealroomStatus, deleteDealroom, listDealrooms (with
per-room session, visitor and last-seen stats), getDealroom, addDealroomItem, removeDealroomItem,
reorderDealroomItems, registerDealroomUpload, getDealroomAnalytics.

Public read functions that call only the SECURITY DEFINER RPCs and never require auth.
Asset functions: listAssets across every kind, createAssetLink. A deck-registry sync function.

Tracking endpoints as raw routes (called with navigator.sendBeacon): one for share-link sessions, one
for room sessions. Accept start/heartbeat/event payloads, validate with Zod, apply the rate limit,
store a hashed user-agent digest rather than the raw string, and never return personal data.
```

**Verify:** create a room by prompting, read it back; confirm no service-role code is in the browser bundle.

## Part 5: Deal rooms

### Step 10. The public deal room

```text
Build the public deal room at /room/$slug. It is the only page a prospect sees, so it must be
immaculate on a phone.

Layout, in a single centered column:
1. Header: my logo × the customer's logo, white on the dark background, and a small accent label
   "Deal room · ".
2. Welcome message: a chat-bubble card with my headshot, name, role and markdown welcome copy. Its
   sidebar label reads "From ".
3. Process overview: a horizontal stage tracker from process_steps, each step done, current or
   upcoming. On mobile, only the active stage label and a compact progress indicator.
4. Asset workspace: a left sidebar of items (thumbnail, title, description) and a right pane rendering
   the selected item. The divider is draggable; the sidebar is wider than a typical nav.

Item kinds: deck, one-pager, template instance, uploaded PDF, external link or video, markdown note, app.

Mobile contract, for every asset in every room:
- Below 900px: single column, welcome first, then tracker, item list, selected item.
- Decks swipeable and scaled to fit; PDFs rendered in-app page by page; fixed-width pages scaled to
  fit; videos responsive 16:9; everything legible down to 360px.

The page loads through the public server function. Draft or archived rooms render a clean not-found
page. Page title format " × ".
```

**Verify:** in incognito at desktop width and at 402px, every item opens, the sidebar resizes, nothing overflows.

### Step 11. The item deep link that cannot leak

```text
Add /room/$slug/a/$itemId for a single item, resolved server-side through resolve_public_dealroom_item
so an item id from a different room cannot render.

With ?embed=1 or inside an iframe, render the asset and nothing else: no workspace navigation, no "back
to library" link, no logo linking home. Then audit the whole app: every route reachable from a share
link hides its workspace chrome in embedded mode, and the 404 page offers no link back to the workspace.
```

**Verify:** open the deep link in incognito; there is no clickable path to /.

### Step 12. Thumbnails

```text
Every room item needs a thumbnail that previews the actual asset, never a generic icon:
- Deck: its title slide. PDF: page 1. Loom/YouTube: the video's own still.
- One-pager or template instance: the first page. App: its first screen.

Implement it: a headless browser screenshots the source route, the image is saved as a project asset,
and its URL goes into display_thumbnail_url. For links, fetch the provider's oEmbed still.
Remember these thumbnail rules permanently.
```

## Part 6: Sharing and tracking

### Step 13. Tokenized share links

```text
Build tokenized sharing for every asset kind and for whole rooms.

- createShareLink (high-entropy URL-safe token), listShareLinks, revokeShareLink (sets revoked_at).
- /s/$token resolves the token, renders the target in embedded mode, and shows a clean "this link is
  no longer active" page when revoked or unknown. Optional email capture when require_email is set.
- A reusable ShareLinksManager on every /links route: create a link with recipient name and note,
  copy it, see view counts, revoke it.
- Tracking: start a session, heartbeat while visible, record slide changes and item opens via
  sendBeacon. Hashed user-agent only, never an IP address. Rate-limited.
- Analytics per asset and per room: unique visitors, sessions, total time, per-item opens.

Revoking a token does not break a plain workspace URL such as /decks/. To kill a leaked plain
URL, rename the slug so the old path 404s. Tell me that whenever I ask to revoke access.
```

**Verify:** create a link, open it in incognito, see tracking rows, revoke it, see it die.

### Step 13b. Visit webhooks (needed for Swan alerts)

```text
Add outbound webhooks. A connection has an HTTPS URL, a signing secret (shown once), the events it
wants and a coverage mode (only watched rooms, new rooms, or all rooms). Events: dealroom.visit_started
and dealroom.visit_summary (sent when a visit ends: room, viewer email if captured, country, device,
active seconds, and each item viewed with seconds). Sign every delivery, retry failures with backoff,
keep a delivery log, and never fire for preview or internal traffic unless asked.
```

## Part 7: Making the agent your operator

### Step 14. The MCP server and HTTP API

```text
Add an MCP server so an AI assistant can read the asset library and act on rooms, plus equivalent
HTTP endpoints under /api/public/* for agents that call HTTP. Protect both with a bearer secret.

One tool per file, registered in one entry that is import-safe (no env reads, no I/O at module scope).
Tools:
- list_assets (every reusable asset, each with a one-sentence "when to use this" description and an
  audience flag: collateral vs made for one customer), list_decks, list_onepagers, list_apps,
  list_templates, get_template_brief.
- find_dealrooms (by company name or domain), get_dealroom, create_dealroom (idempotent: one room per
  company), update_dealroom, manage_dealroom_items (add, remove, reorder, update), get_dealroom_url
  (returns the plain url and a tracked, email-gated share url).
- add_asset_link, register_webhook, list_webhooks.

Accurate titles, one-sentence descriptions and correct annotations on every tool. Handlers return
quickly. Regenerate and validate the MCP manifest.
```

**Verify:** the manifest lists every tool; "create a room for X with these assets" works through the API.

### Step 15. The create-dealroom skill

```text
Create a project skill called create-dealroom with the full checklist for building a room:
1. Customer logo: never null, never hotlinked. Resolve the brand, pick the largest wordmark, normalize
   it to white on transparent, visually inspect it on the dark background, re-host it, store that URL.
2. Room defaults: brand accent only, never a per-customer color. Status live when I intend to share.
   Stages , exactly one current, earlier ones done. Welcome from me, labelled
   "From ".
3. Assets: in the order I list them, thumbnail rules applied, mobile-legible to 360px, opened through
   the embedded item route.
4. Before reporting done: read the room back, load it at 402px and desktop in a headless browser,
   confirm the logo and every thumbnail, and remind me that frontend changes need a manual publish.
```

### Step 16. Memory rules

```text
Save these as permanent project rules:
- Brand accent is . Never a per-customer accent color.
- Thumbnail rules: deck title slide, PDF page 1, video still, one-pager first page, app first screen.
- Every asset in every room is mobile-first and legible to 360px. Desktop-only fixes are regressions.
- Copy rules: .
- No end-user auth. All operator actions are server functions invoked by prompting.
- Frontend changes require a manual publish before a customer can see them.
```

## Part 8: Operating it day to day

| Job | Prompt |
| --- | --- |
| Create a room | "Create a new deal room for \ (\). Stage \. Include \, \, \ in this order. Add a welcome note from me referencing \." |
| Duplicate an asset | "Duplicate the \ deck for \. Change the pricing to the attached screenshot. Keep everything else identical. Read numbers from the image." |
| Change one thing | "On slide \ of the \ deck, replace \ with \. Change nothing else." |
| Share | "Create a share link for \ for \, and give me the URL." |
| Revoke | "Revoke the share link for \. If the recipient has a plain workspace URL, rename the slug and tell me the new one." |
| Publish and verify | "Confirm the room is live, walk it in a headless browser at 402px and desktop, and show me screenshots of every item." |

## Appendix: verification by phase

| Phase | Passing looks like |
| --- | --- |
| Foundation | /design-system/tokens renders live swatches; one CSS variable changes everything |
| Assets | Each asset kind appears in /assets with a thumbnail and a share action |
| Data | Security linter clean; anonymous read of dealrooms is denied |
| Server | A room can be created and read back purely by prompting; no service-role code in the client bundle |
| Deal room | Correct in incognito at 402px and desktop; no path back to the workspace |
| Sharing | Link tracks views, revocation kills it, analytics roll up, webhooks deliver |
| Agent | "Create a room for X with these assets" produces a correct room with no follow-up corrections |

The thing that makes this work is not any single feature. Every asset is registered, every registry is readable by the agent, and every rule that matters is stored as memory or a skill instead of being re-explained.
