# Build with Lovable

> Build a signup-to-campaign app in Lovable: form submission, journey enrollment, behavioral events, and admin observability.

This guide walks through building a working app on Caramel using Lovable's AI assistant as the integration layer. By the end, you have a signup form that creates a contact in your Caramel audience and enrolls them in a welcome journey.

The example app is a coffee-shop newsletter signup called **BrewClub**. Estimated time: 30 minutes.

> **Note** This guide assumes basic Lovable familiarity. If you're new to Lovable, complete their getting-started tutorial at [lovable.dev](https://lovable.dev) first.

## Why MCP for Lovable

Lovable's assistant generates React + TypeScript. It's most effective when it can discover your API's schema rather than having each endpoint spelled out. The MCP connector provides the full tool catalog via `tools/list` at connection time — so you prompt in plain language and Lovable writes the calls.

For server-side integrations (a Node.js backend calling Caramel outside Lovable), use the REST gateway at `gateway.caramelme.com` directly.

## Step 1 — Connect Caramel to your Lovable workspace

1. In Lovable, open **Workspace Settings → Connectors → Add MCP server**.
2. Enter the URL: `https://app.caramelme.com/api/functions/caramel-mcp`
3. Click **Connect**.
4. Complete the magic-link OAuth flow — enter your Caramel email, click the link in your inbox.
5. Lovable returns connected.

Verify by asking Lovable's assistant:

> What Caramel tools do I have available?

The assistant calls `caramel.v1.meta.capabilities` and lists all 19 tools. If nothing comes back, reconnect the connector.

Default scopes granted: `meta:read`, `forms:read`, `forms:write`, `audience:read`. Add `audience:write` when you need to upsert contacts directly.

## Step 2 — Configure Caramel (one time, ~5 minutes)

Do this once per business in the Caramel dashboard.

1. **Create a form** — Open **Forms → New form**. Add these fields:
   - `email` (required, type email)
   - `first_name` (required, type text)
   - `favorite_drink` (optional, type text)
   - `subscribe_to_emails` (optional, type checkbox)
2. **Activate the form.** Note the form ID (shown in **Forms → Share** — looks like `frm_01h7…`).
3. **Verify a sender domain** — **Settings → Domains → Add domain**. Publish the DNS records Caramel provides (DKIM, SPF, DMARC). Verification takes 5–30 minutes after DNS propagates.
4. **Activate a welcome journey** — **AI Agent → Template Library**, search "welcome series" for your industry, click **Activate**.

That's the Caramel side. No Caramel-specific code required.

## Step 3 — Generate the signup page

In Lovable's chat:

> Build a coffee-shop newsletter signup page for BrewClub. Header with the BrewClub logo, hero section with "Sign up for fresh-roast news and a free coffee", and a form with fields for email, first name, and favorite drink, plus a "Yes, send me news and offers" checkbox. Style with warm brown tones. Use shadcn/ui components.

Iterate with follow-up prompts as needed:

- "Add a success state showing 'Check your inbox!' after submission."
- "Require email and first name. Validate email format."
- "Add a loading spinner during submission."

## Step 4 — Wire the form to Caramel

Prompt Lovable:

> When the form is submitted, call `caramel.v1.form.submit` via the MCP connector. Use form ID `frm_01h7…`. Pass email, first_name, favorite_drink, and subscribe_to_emails. Include the current URL as source_url. Show success state on success, error toast on failure.

Lovable generates a handler like this:

```tsx
const handleSubmit = async (formData: FormData) => {
  setLoading(true);
  try {
    const res = await fetch('https://app.caramelme.com/api/functions/caramel-mcp', {
      method:  'POST',
      headers: {
        'Authorization': `Bearer ${import.meta.env.VITE_CARAMEL_TOKEN}`,
        'Content-Type':  'application/json',
      },
      body: JSON.stringify({
        jsonrpc: '2.0',
        method:  'tools/call',
        id:      Date.now(),
        params: {
          name: 'caramel.v1.form.submit',
          arguments: {
            form_id: 'frm_01h7…',
            data: {
              email:               formData.email,
              first_name:          formData.firstName,
              favorite_drink:      formData.favoriteDrink,
              subscribe_to_emails: formData.subscribeToEmails,
            },
            source_url: window.location.href,
          },
        },
      }),
    });

    const result = await res.json();
    if (result.error || result.result?.isError) {
      toast({ title: 'Something went wrong', description: 'Please try again.', variant: 'destructive' });
      return;
    }
    setSuccess(true);
  } catch {
    toast({ title: 'Network error', description: 'Please check your connection.', variant: 'destructive' });
  } finally {
    setLoading(false);
  }
};
```

A few things to note about this code:

- **`VITE_CARAMEL_TOKEN`** — Lovable's connector injects and refreshes this automatically.
- **MCP envelope** — every call needs `jsonrpc`, `method`, `id`, and `params`.
- **Error handling** — MCP tool errors surface as `result.result.isError === true`. Protocol errors surface as `result.error`. Check both.

## Step 5 — Test the end-to-end flow

Click **Preview** in Lovable, fill the form with your own email, and submit.

Expected behavior:
1. Loading state for ~300 ms.
2. Success state with "Check your inbox!".
3. Welcome email arrives within 30 seconds.

**Troubleshooting:**

| Symptom | Likely cause | Fix |
|---|---|---|
| `Authentication required` toast | OAuth token expired | Reconnect the Caramel connector in **Workspace Settings** |
| `form_not_found` in console | Wrong form ID or form is inactive | Run `caramel.v1.form.list` to get the correct ID |
| Success state but no email | Sender domain not verified | Run `caramel.v1.domain.status` — check DNS propagation |
| `validation` error | Required field missing | Verify `email` and `first_name` are present in the submission |

## Step 6 — Track behavioral events

When a customer hits a milestone (100th order, anniversary), upsert updated traits:

```tsx
async function trackMilestone(email: string) {
  await fetch('https://app.caramelme.com/api/functions/caramel-mcp', {
    method:  'POST',
    headers: {
      'Authorization': `Bearer ${import.meta.env.VITE_CARAMEL_TOKEN}`,
      'Content-Type':  'application/json',
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method:  'tools/call',
      id:      Date.now(),
      params: {
        name: 'caramel.v1.contact.upsert',
        arguments: {
          business_id: import.meta.env.VITE_CARAMEL_BUSINESS_ID,
          email,
        },
      },
    }),
  });
}
```

> **Plan** `caramel.v1.contact.upsert` requires the Business tier or above. Starter and Growth callers receive `403 tier_required`. For custom trait writes on lower tiers, submit to a dedicated "milestone" form instead — form submit works on every tier.

## Step 7 — Add admin observability

Show the business owner this month's signup count:

> Add an admin panel at `/admin` that shows this month's signup count and remaining AI credits from `caramel.v1.meta.usage`. Refresh every 10 minutes.

Lovable generates:

```tsx
useEffect(() => {
  const fetchUsage = async () => {
    const res = await fetch('https://app.caramelme.com/api/functions/caramel-mcp', {
      method:  'POST',
      headers: {
        'Authorization': `Bearer ${import.meta.env.VITE_CARAMEL_TOKEN}`,
        'Content-Type':  'application/json',
      },
      body: JSON.stringify({
        jsonrpc: '2.0',
        method:  'tools/call',
        id:      Date.now(),
        params: {
          name: 'caramel.v1.meta.usage',
          arguments: { business_id: import.meta.env.VITE_CARAMEL_BUSINESS_ID },
        },
      }),
    });
    const { result } = await res.json();
    setUsage(JSON.parse(result.content[0].text));
  };
  fetchUsage();
  const id = setInterval(fetchUsage, 10 * 60 * 1000);
  return () => clearInterval(id);
}, []);
```

Render `usage.forms_submitted_this_month` for signups and `usage.ai_credits_remaining` for credits.

## What BrewClub does at this point

1. Customer fills the form → Caramel creates the contact.
2. Day 0: welcome email from your verified domain.
3. Day 3: follow-up email (configured in the journey).
4. The owner checks the admin panel for signup counts.

## Current gaps and workarounds

> **Coming soon** The following are not yet available:

- **No imperative `email.send`.** You can activate a journey but cannot call `caramel.email.send({ to, subject, body })` directly. Use journey activation instead.
- **No outbound webhooks.** Your app polls for status — use `caramel.v1.meta.usage` or the dashboard. Outbound webhooks are on the roadmap.
- **No programmatic business provisioning.** Multi-tenant SaaS where each customer needs their own Caramel business must redirect customers to `caramelme.com/signup` until API provisioning ships.

## Next steps

- [Embed a form](embed-a-form) — render Caramel forms natively with your own components.
- [Sync contacts](sync-contacts) — bulk-import from a CRM or database.
- [Connect a ChatGPT Custom GPT](connect-chatgpt-actions) — same Caramel backend, different AI client.
- [Authentication](../get-started/authentication) — full OAuth details for when Lovable's connector isn't managing tokens for you.
