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

# Security and Production

> Production guidance for API keys, origins, session lifetimes, iframe behavior, and rollout.

Canvas Embed SDK is designed around a server-created, short-lived iframe session. The browser never receives your Moda API key.

## Server-side API keys only

Do:

* Store the Moda API key in a backend secret manager
* Create, refresh, and revoke sessions on your backend
* Use separate API keys for production, staging, and local development

Do not:

* Put `moda_live_...` keys in frontend code
* Call `POST /v1/embed/sessions` directly from the browser
* Reuse one API key across unrelated customer environments

## Entitlements

The docs and SDK can be visible without granting access. Actual feature access is enforced by Moda:

* The API key must belong to an enabled paid plan or approved beta account
* The key owner must have access to the target canvas
* Edit-mode sessions require edit permission on the canvas

If the account is not enabled, session creation fails before a user can load the iframe.

## Allowed origins

Set `allowed_origins` to the exact origins that host the iframe.

```json theme={null}
{
  "allowed_origins": [
    "https://app.example.com",
    "https://staging.example.com",
    "http://localhost:4000"
  ]
}
```

Rules:

* Include scheme and host
* Include port for local development
* Do not include paths
* Do not use wildcards

The iframe rejects messages from any origin not in this list.

## Treat embed URLs as bearer URLs

The `embed_url` contains a short-lived boot token in the query string. The iframe exchanges it for a narrower browser credential, but the boot token can still load the session while it is valid. Treat the full URL like a temporary secret and assume generic URL tooling can capture it unless you configure that tooling otherwise:

* Do not log it in analytics or error tools
* Redact query strings from reverse proxy, CDN, and application request logs where possible
* Do not store it permanently
* Do not send it to third-party services
* Refresh it before expiry for long sessions
* Revoke it when the user signs out or loses access

For the beta, keep sessions short-lived and only send embed URLs directly from your backend to the browser page that will host the iframe. Do not put embed URLs in emails, tickets, durable notifications, or shared documents.

## Session duration

Sessions support `expires_in_seconds` from 60 to 3600.

Recommended defaults:

| Environment       | Duration                           |
| ----------------- | ---------------------------------- |
| Local development | 3600 seconds                       |
| Customer editor   | 1800 to 3600 seconds, with refresh |
| One-off preview   | 300 to 900 seconds                 |

For long-running editors, refresh from your backend and send `session.refresh` to the iframe before expiry.

## Iframe attributes

Use these attributes — the embed relies on them:

```html theme={null}
<iframe
  src="EMBED_URL"
  title="Moda canvas"
  allow="clipboard-read; clipboard-write; fullscreen"
  referrerpolicy="strict-origin-when-cross-origin"
  style="width: 100%; height: 100%; border: 0"
></iframe>
```

| Attribute                                          | Why                                                                                                 |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `allow="clipboard-read; clipboard-write"`          | Copy/cut/paste inside the editor, including pasted image files and screenshots                      |
| `allow="...; fullscreen"`                          | Reserved for fullscreen editing surfaces                                                            |
| `referrerpolicy="strict-origin-when-cross-origin"` | Lets the iframe infer the parent origin for the `ready` handshake without leaking the full host URL |

You do **not** need a `downloads` permission. Export never downloads from inside the iframe — the iframe returns a base64 `dataUrl` via `postMessage` and **your page** triggers the download. Image upload uses the iframe's own `<input type="file">` picker, which needs no special `allow` token.

Avoid adding a restrictive `sandbox` attribute unless you have tested it with your required features. If your security policy requires sandboxing, test at least:

* Scripts
* Same-origin behavior
* Clipboard paste
* File picker and image upload

## Content Security Policy

Your parent application must allow the Moda iframe origin:

```http theme={null}
Content-Security-Policy: frame-src https://app.moda.app;
```

If your parent page calls your own backend only, no additional `connect-src` is needed for Moda from the parent page. The iframe performs its own API calls from the Moda origin.

The embed does not gate framing with a server `frame-ancestors` header. Instead it enforces the `allowed_origins` you set at session creation **at runtime**, on every `postMessage`: the iframe ignores messages from, and refuses to post to, any origin not in that list. Your page still owns the download and clipboard UX, since exports and copy/paste resolve in the parent.

## Plan for failures

Handle these cases in your parent app:

| Failure                      | What to do                                                                                                                                                                          |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Session create returns `403` | Show "Embedding is not enabled" or ask the user to contact an admin                                                                                                                 |
| Iframe emits `load:error`    | Branch on the `status`/`code` ([error-code table](/canvas-embed/session-api#error-codes)): re-mint the session on `401`, surface a permission error on `403`, offer retry otherwise |
| `canvas:save_error`          | Keep the editor open and retry manual save                                                                                                                                          |
| `export:failed`              | Offer retry or use server-side Public API export                                                                                                                                    |
| `chat:error`                 | Show the message and allow a new prompt                                                                                                                                             |
| Session expires              | Refresh from backend and send `session.refresh`, or mint a new session                                                                                                              |

## Local development

For local cross-origin testing:

1. Run your app on one origin, for example `http://localhost:4000`.
2. Run Moda on another origin, for example `http://localhost:3000`.
3. Create the embed session with `allowed_origins: ["http://localhost:4000"]`.
4. Load the returned `embed_url` in an iframe.

This mirrors production behavior because `postMessage` origin checks are active.

## Production checklist

* API key stored server-side only
* Paid-plan or beta entitlement enabled
* `allowed_origins` are exact production and staging origins
* Parent app validates `channel`, `version`, `sessionId`, `origin`, and `source` on every message
* Parent app handles `load:error`, save errors, export errors, chat errors, and expiry
* Session refresh and revoke endpoints exist in your backend
* Embed URL query strings are excluded from logs and analytics
* Browser-token responses are only passed to the iframe through `session.refresh`
* Manual QA covers edit, autosave, export, image paste/upload, keyboard undo/redo, and chat if enabled
