Embed Iframe Services

Issue short-lived signed iframe URLs for clients, then render scoped services inside the platform app at /embed/[service].

Current service support:

  • files (enabled)
  • notif and tasks are scaffolded and return not enabled for now

Personas

  • Create and manage embed_clients records (API key hash, allowed origins, allowed scopes, service allowlist).
  • Configure EMBED_SIGNING_SECRET in both projects and keep values identical.
  • Validate bucket CORS + IAM to match the scoped embed access model.

Overview

This feature is split across both projects:

  • audla-apis-on-vercel
    • POST /api/embed/url authenticates a client via X-Api-Key
    • Validates service + scope against embed_clients allowlists
    • Signs a short-lived token with EMBED_SIGNING_SECRET
    • Returns iframe URL: ${PLATFORM_BASE_URL}/embed/${service}?t=<token>
  • platform
    • /embed/[service] verifies the token and checks requested service
    • Validates parent origin (referer/origin) against token origins
    • Renders a minimal service UI (for Files: bucket-scoped FileExplorer)

Setup responsibilities

Use this split to avoid configuration gaps:

  • audla-apis-on-vercel owns URL issuance
    • Authenticates client integrations via X-Api-Key.
    • Enforces embed_clients allowlists (allowed_services, allowed_origins, allowed_scopes).
    • Signs short-lived iframe tokens and returns the URL.
  • platform owns embed rendering and S3 presign
    • Verifies token signature, expiry, service, and parent origin.
    • Renders /embed/[service] for valid requests.
    • Issues short-lived S3 presigned upload/download URLs for the files service.
  • Client application owns iframe lifecycle
    • Calls POST /api/embed/url from backend only.
    • Mounts iframe with the returned URL.
    • Handles token refresh message flow and re-signed token delivery.

Platform setup checklist

Complete these steps in the platform project:

  1. Set shared signing secret

    • Configure EMBED_SIGNING_SECRET (or AUDLA_EMBED_SIGNING_SECRET).
    • Value must exactly match audla-apis-on-vercel EMBED_SIGNING_SECRET.
  2. Configure S3 signer credentials

    • Set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY.
    • Confirm signer credentials have s3:GetObject and s3:PutObject (and s3:ListBucket if your UI needs listing).
  3. Confirm embed and presign endpoints are reachable

    • GET /embed/files?t=<token>
    • POST /api/embed/s3/presign-upload
    • POST /api/embed/s3/presign-download
  4. Align bucket CORS with embed usage

    • Allow PUT, GET, HEAD.
    • Allow platform origin + each client app origin that hosts the iframe.
    • Keep origins explicit in production.
  5. Verify unauthorized behavior

    • Invalid/expired token, wrong service, or disallowed origin must render unauthorized embed state.

Client app setup checklist

Complete these steps in the client application:

  1. Provision integration credentials (once)

    • Store raw client API key securely server-side.
    • Create/maintain embed_clients policy in audla-apis-on-vercel (services, origins, scopes).
  2. Issue iframe URL from backend

    • Call POST /api/embed/url with:
      • X-Api-Key: <raw_client_api_key>
      • service: "files"
      • scope: { "bucket": "<approved-bucket>" }
      • optional expiresInSeconds (max 3600)
    • Never call this endpoint from browser code.
  3. Render iframe with returned URL

    • Set iframe.src to returned url.
    • Ensure your app origin is in embed_clients.allowed_origins and therefore present in token origins.
  4. Implement token refresh postMessage flow

    • Listen for { type: "audla.embed.token-expired", service: "files" } from iframe.
    • Request a new URL/token from backend.
    • Reply to iframe with { type: "audla.embed.token-refreshed", token: "<new-token>" }.
  5. Use scope-compatible requests only

    • Bucket/path operations requested inside the iframe must remain inside the signed scope.
    • Out-of-scope requests will be denied by presign endpoints.

Architecture


Issue embed URL

Endpoint

  • POST /api/embed/url
  • Headers:
    • Content-Type: application/json
    • X-Api-Key: <raw_client_api_key>

Body

{
  "service": "files",
  "scope": {
    "bucket": "client-files-bucket"
  },
  "expiresInSeconds": 900
}

expiresInSeconds is optional (default 900), clamped to max 3600.

Success response

{
  "url": "https://platform.audla.ca/embed/files?t=eyJ...snip",
  "expiresAt": 1760000000,
  "service": "files"
}

Presigned S3 endpoints

The platform embed flow supports direct S3 transfer with short-lived URLs:

  • POST /api/embed/s3/presign-upload
  • POST /api/embed/s3/presign-download

Both endpoints require:

  • token (the iframe token from t=...)
  • bucket/key or folder inputs

The server validates:

  • token signature + expiry
  • service is files
  • requested bucket/path is inside embed scope (bucket, optional path, optional uploadFolder)

Upload request example:

{
  "token": "<embed_token>",
  "bucket": "client-files-bucket",
  "folderPath": "/uploads",
  "fileName": "invoice.pdf",
  "contentType": "application/pdf"
}

Token payload

Token format:

base64url(payload) + "." + base64url(hmac_sha256(payload, EMBED_SIGNING_SECRET))

Payload fields:

  • cid: embed client id (embed_clients.id)
  • companyId: linked company id
  • svc: service key (files)
  • scope: service scope ({ bucket } for files)
  • origins: allowed iframe parent origins
  • iat, exp: issued-at and expiry (unix seconds)

Client provisioning model

Access control is driven by embed_clients in Postgres:

  • api_key_hash: SHA-256 hash of client key (raw key is never stored)
  • allowed_services: array allowlist (e.g. ["files"])
  • allowed_origins: array allowlist (e.g. ["https://client.example.com"])
  • allowed_scopes: JSON policy per service (e.g. allowed buckets for files)
  • revoked_at: if set, API key is disabled

Files scope policy currently checks:

  • allowed_scopes.files.buckets[] contains requested scope.bucket
  • optional allowed_scopes.files.paths[] prefix-match for browsing/download scope
  • optional allowed_scopes.files.uploadFolders[] prefix-match for upload scope

S3 bucket setup

For iframe uploads/downloads with presigned URLs, configure the bucket CORS and IAM carefully.

Required CORS configuration

Set bucket CORS to include your platform origin and all client app origins that host the iframe:

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "HEAD"],
    "AllowedOrigins": [
      "https://platform.audla.ca",
      "https://client.example.com"
    ],
    "ExposeHeaders": ["ETag", "x-amz-request-id"],
    "MaxAgeSeconds": 3000
  }
]

Notes:

  • Include every client origin that embeds the iframe.
  • Keep origins explicit (avoid * for production).
  • PUT is needed for presigned uploads; GET for downloads.

IAM permissions for signing server

The credentials used by Platform should be able to:

  • s3:GetObject
  • s3:PutObject
  • s3:ListBucket (if listing is needed)

Scope these permissions to the specific client buckets/prefixes whenever possible.

Public access settings

  • If you use presigned URLs for access control, keep Block Public Access enabled where possible.
  • Do not rely on public object ACLs for embedded clients.

Environment variables

audla-apis-on-vercel

  • EMBED_SIGNING_SECRET (required)
  • PLATFORM_BASE_URL (required, e.g. https://platform.audla.ca)
  • POSTGRES_URL (required for embed_clients lookup)

platform

  • EMBED_SIGNING_SECRET (must exactly match API project)
  • AWS_REGION (required for S3 presign)
  • AWS_ACCESS_KEY_ID (required for S3 presign)
  • AWS_SECRET_ACCESS_KEY (required for S3 presign)

Security notes

  • Keep X-Api-Key server-side only; never expose it in browser code.
  • Use short TTLs for generated URLs (15 minutes recommended).
  • Restrict allowed_origins and allowed_scopes as tightly as possible.
  • Token verification and origin checks happen before rendering embedded content.
  • Existing file APIs in platform are still being hardened separately for stricter bucket enforcement.

Was this page helpful?