> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mareaalcalina.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Storefronts

> What a storefront is in Marea, how it is created and updated, the previewUrl / publicUrl split, and cross-tenant access rules.

# Storefronts

A **storefront** is the unit of catalog ownership in Marea. One user owns one or more storefronts; each has a name, a category list, products, branding, contact info, business hours, delivery settings, and a publishable hosted URL.

Under the hood a storefront is the `users/{ownerUid}/menus/{menuId}` document; the API surface presents it with the typed id `stf_<menuId>`. Internal ids never leak across the API — every handler strips the `stf_` prefix at the top before any Firestore access.

| Concept       | API surface         | Notes                                      |
| ------------- | ------------------- | ------------------------------------------ |
| Storefront id | `stf_<token>`       | Opaque, agent-stable                       |
| Product id    | `prd_<token>`       | Opaque, agent-stable                       |
| Preview link  | `_links.previewUrl` | 24h token, anyone with the link can open   |
| Public link   | `_links.publicUrl`  | `null` until publish; stable after publish |
| Edit link     | `_links.editUrl`    | Dashboard deep link for the owner          |

## Lifecycle

```
created (draft)        ←─ POST /v1/storefronts
   │
   ├─ previewUrl available immediately (24h-token preview)
   │
   └─ POST /v1/storefronts/:storefrontId/publish ──▶ public live URL
                                                         │
                                                         ├─ 402 if pre-paywall account
                                                         ├─ 422 if 0 products
                                                         └─ 451 if ToS not accepted
```

A storefront is created in **draft** state. Anyone with the `previewUrl` can open it; the URL token expires after 24 hours. To make it public, call `POST /v1/storefronts/:storefrontId/publish` (see [Publishing](/concepts/publishing)).

## Creating a storefront

`POST /v1/storefronts` accepts a `StorefrontManifest`. Required scope: `catalog:write` (user keys only).

```http theme={null}
POST /v1/storefronts HTTP/1.1
Authorization: Bearer mk_user_...
Idempotency-Key: 2f1a8c4b-2e3a-4b9d-9f1a-8c4b2e3a4b9d
Content-Type: application/json

{
  "name": "Tacos La Marea",
  "businessType": "restaurante",
  "language": "es",
  "currency": "MXN",
  "categories": [
    { "title": "Tacos", "description": "Estilo tradicional" },
    { "title": "Bebidas", "description": null }
  ],
  "products": [
    { "title": "Taco al pastor", "price": 25, "imageUrl": "https://...", "category": "Tacos" },
    { "title": "Coca Cola",       "price": 30, "imageUrl": null,         "category": "Bebidas" }
  ],
  "schedule": [{ "day": "mon", "open": "08:00", "close": "22:00" }]
}
```

* `products` is optional in the manifest. To batch-add more after creation, use the product endpoints.
* If the manifest contains more products than the plan allows, the response is **`207 Multi-Status`** with the storefront created up to the plan cap plus an `errors` array describing the over-cap items. See [Plan limits](/concepts/plan-limits).
* Idempotency: send `Idempotency-Key` to make retries safe. See [Safe mutations](/concepts/safe-mutations).

Response (`StorefrontDto`):

```json theme={null}
{
  "storefront": {
    "id": "stf_abc123",
    "name": "Tacos La Marea",
    "language": "es",
    "currency": "MXN",
    "published": false,
    "categories": [{ "title": "Tacos", "description": "Estilo tradicional" }],
    "products": [{ "id": "prd_xyz", "title": "Taco al pastor", "price": 25 }],
    "_links": {
      "previewUrl": "https://marea.pro/preview/<token>",
      "publicUrl": null,
      "editUrl": "https://mareaalcalina.com/dashboard/menu/stf_abc123"
    }
  }
}
```

`_links.publicUrl` is `null` until publish — never expose the preview URL as the "public" URL.

## Updating a storefront

`PATCH /v1/storefronts/:storefrontId` is a **partial** update. Required scope: `catalog:write`. Three merge rules:

| Field shape     | Behavior                                                                 |
| --------------- | ------------------------------------------------------------------------ |
| Nested object   | Deep-merge. `{ "delivery": { "fee": 50 } }` only changes `delivery.fee`. |
| Array           | Full-replace. `{ "categories": [...] }` overwrites the whole array.      |
| Explicit `null` | Clears the field. `{ "delivery": null }` removes the delivery config.    |

Returns the updated `StorefrontDto` (`200 OK`). Republishing is a separate call — `PATCH` only mutates the draft.

## Products

Products live on the storefront. Use `POST /v1/storefronts/:storefrontId/products` to create and `PATCH /v1/storefronts/:storefrontId/products/:productId` to update. Both require `catalog:write` and accept `Idempotency-Key`. The per-plan product ceiling is enforced on every create; over-cap creates return `402 plan_limit` with `upgrade.upgradeUrl` populated.

## Cross-tenant access is silently denied

If your `mk_user_*` key tries to access a storefront owned by a different user, the API returns **`404 storefront_not_found`** — not `403`. The 404 also fires before the empty-storefront check on publish to avoid leaking existence by status-code timing.

Common cause when you get a 404 on a stable `stf_` id you used moments ago: you're sending the wrong user key (e.g. dev environment vs. prod). Confirm with `GET /v1/me` which user the key acts as.

## What a storefront does NOT include

* A storefront is **not** a channel. Where customers reach you (WhatsApp, web, in-person) is per-storefront configuration; the channel mix is not surfaced as a top-level field today.
* A storefront is **not** an order ledger. Orders are a separate resource — subscribe to `order.created`, `order.status_updated`, `order.paid` via [Webhook endpoints](/concepts/webhooks).
* Storefronts do not own subscriptions. Billing/plan lives on the owning user (see `/v1/me`).

## Limits

See [Plan limits](/concepts/plan-limits) for per-plan storefront-count and product-count ceilings. The product cap is per-storefront, not per-account.

## Verification in code

* `src/api/v1/storefronts.create.ts`
* `src/api/v1/storefronts.update.ts`
* `src/api/v1/storefronts.publish.ts`
* `src/api/services/translation/storefront-id.ts` (`stf_` ↔ menuId translation)
* `src/api/services/shape/storefront.shape.ts` (`toStorefrontDto`)
