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)notifandtasksare scaffolded and return not enabled for now
Personas
- Create and manage
embed_clientsrecords (API key hash, allowed origins, allowed scopes, service allowlist). - Configure
EMBED_SIGNING_SECRETin 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-vercelPOST /api/embed/urlauthenticates a client viaX-Api-Key- Validates service + scope against
embed_clientsallowlists - 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 tokenorigins - Renders a minimal service UI (for Files: bucket-scoped
FileExplorer)
Setup responsibilities
Use this split to avoid configuration gaps:
audla-apis-on-vercelowns URL issuance- Authenticates client integrations via
X-Api-Key. - Enforces
embed_clientsallowlists (allowed_services,allowed_origins,allowed_scopes). - Signs short-lived iframe tokens and returns the URL.
- Authenticates client integrations via
platformowns 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
filesservice.
- Client application owns iframe lifecycle
- Calls
POST /api/embed/urlfrom backend only. - Mounts iframe with the returned URL.
- Handles token refresh message flow and re-signed token delivery.
- Calls
Platform setup checklist
Complete these steps in the platform project:
-
Set shared signing secret
- Configure
EMBED_SIGNING_SECRET(orAUDLA_EMBED_SIGNING_SECRET). - Value must exactly match
audla-apis-on-vercelEMBED_SIGNING_SECRET.
- Configure
-
Configure S3 signer credentials
- Set
AWS_REGION,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY. - Confirm signer credentials have
s3:GetObjectands3:PutObject(ands3:ListBucketif your UI needs listing).
- Set
-
Confirm embed and presign endpoints are reachable
GET /embed/files?t=<token>POST /api/embed/s3/presign-uploadPOST /api/embed/s3/presign-download
-
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.
- Allow
-
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:
-
Provision integration credentials (once)
- Store raw client API key securely server-side.
- Create/maintain
embed_clientspolicy inaudla-apis-on-vercel(services, origins, scopes).
-
Issue iframe URL from backend
- Call
POST /api/embed/urlwith:X-Api-Key: <raw_client_api_key>service: "files"scope: { "bucket": "<approved-bucket>" }- optional
expiresInSeconds(max 3600)
- Never call this endpoint from browser code.
- Call
-
Render iframe with returned URL
- Set
iframe.srcto returnedurl. - Ensure your app origin is in
embed_clients.allowed_originsand therefore present in tokenorigins.
- Set
-
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>" }.
- Listen for
-
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/jsonX-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-uploadPOST /api/embed/s3/presign-download
Both endpoints require:
token(the iframe token fromt=...)- bucket/key or folder inputs
The server validates:
- token signature + expiry
- service is
files - requested bucket/path is inside embed scope (
bucket, optionalpath, optionaluploadFolder)
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 idsvc: service key (files)scope: service scope ({ bucket }for files)origins: allowed iframe parent originsiat,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 requestedscope.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). PUTis needed for presigned uploads;GETfor downloads.
IAM permissions for signing server
The credentials used by Platform should be able to:
s3:GetObjects3:PutObjects3: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 forembed_clientslookup)
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-Keyserver-side only; never expose it in browser code. - Use short TTLs for generated URLs (15 minutes recommended).
- Restrict
allowed_originsandallowed_scopesas 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.