Connections, remote mode and headless operation
A connection is a saved other instance (name, address, API token with write permission there). Connections belong to the local instance, are only accessible via an admin session 👤 and are not in the bundle; the token is never returned (only token_prefix).
{ "id": 1, "name": "Live-Server", "url": "https://www.firma.de", "token_prefix": "ic_Ab3dEf4",
"last_sync_at": 1791313300, "last_sync": "push", "created_at": 1791313195 }
| Endpoint | Description |
|---|---|
GET /connections |
All connections |
POST /connections |
{ "name", "url", "token" } → 201. The address is normalized (without a scheme: https://) |
PUT /connections/{id} |
Change name, address, token (empty token = unchanged). A different address resets the sync state |
DELETE /connections/{id} |
Delete → 204 |
GET /connections/{id}/status |
Queries the server: reachable, error, remote (response of the server’s /bundle/info), local_schema, synced, local_changed / remote_changed (content here or there changed since the last sync, live data excluded), local_version, local_commit, version_mismatch (different schema or different version – for development builds, a different commit) |
POST /connections/select |
{ "id": 1 } switches remote mode on, { "id": 0 } off → 204 |
Remote mode: select sets the cookie ic_remote. As long as it is set, the instance forwards all requests of an admin session to /api/v1/…, /media/… and /_assets/… with the saved token to the connection’s server – the admin then works live with it. Excluded (stay local): /auth/…, /users, /tokens, /connections…, /bundle/push, /bundle/pull. Write requests need X-IC-CSRF as usual; without a session, nothing is forwarded. GET /auth/status then contains remote (the connection). Forwarded responses carry Cache-Control: no-store, because the same address comes from a different instance depending on the selection.
Headless operation: without admin app, login and sessions – fixed via IC_HEADLESS=1 or -headless, or switchable via PUT /system. /cms-admin, /auth/… and all 👤 endpoints return 404, session cookies are ignored. Website, token API, HTTP inputs and public pens run normally; /healthz and /bundle/info report "headless": true. illusioncms -data <ordner> -create-token "Name" creates a token (write permission, output on stdout).
Master token (IC_MASTER_TOKEN, at least 32 characters): acts like an API token with write permission (header or, for GET, ?token=), without a user account and without an entry in the database – for servers that are only maintained via a connection, and as an emergency exit. Account and token management (👤) remain closed to it. Author in the history: Master-Token.
GET /system ✏️ · PUT /system ✏️
Operation of this instance – also via token, i.e. from another instance through a connection.
{ "admin_enabled": true, "forced": false, "write_tokens": 2, "master_token": false, "version": "v0.1.9" }
PUT with { "admin_enabled": false } turns the admin off (saved, also applies after a restart), true turns it back on. 409 if the admin is disabled via a startup option (forced) or if, when turning it off, there is neither a token with write permission nor a master token (otherwise nobody could get in anymore). On the server itself, illusioncms -enable-admin turns it back on.
Security headers
GET /security ✏️ · PUT /security ✏️ – settings of this instance (not in the bundle) and the headers generated from them. PUT is a merge update.
{
"settings": {
"referrer_policy": "strict-origin-when-cross-origin",
"framing": "sameorigin",
"frame_domains": [],
"hsts": false, "hsts_max_age": 31536000, "hsts_subdomains": false, "hsts_preload": false,
"permissions_policy": false,
"csp": "", "csp_report_only": false,
"hide_version": false
},
"headers": {
"all": [{ "name": "X-Content-Type-Options", "value": "nosniff" }, { "name": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }],
"site": [{ "name": "X-Frame-Options", "value": "SAMEORIGIN" }, { "name": "Content-Security-Policy", "value": "frame-ancestors 'self'" }]
}
}
| Field | Values |
|---|---|
referrer_policy |
a value from the Referrer-Policy specification or "" (no header) |
framing |
sameorigin, deny, any (no protection) or domains with frame_domains (https://partner.de, *.partner.de). Sent as X-Frame-Options and/or CSP frame-ancestors; if frame-ancestors is already in csp, that value applies |
hsts* |
max-age 0 to 63072000 s; preload requires hsts_subdomains and at least 1 year. Only over HTTPS (direct or X-Forwarded-Proto: https from a local/private proxy) |
permissions_policy |
blocks camera, microphone, geolocation, payments, USB, serial, Bluetooth, HID, MIDI, Topics |
csp, csp_report_only |
custom Content Security Policy (max. 4000 characters, line breaks become spaces); report_only sends it as …-Report-Only |
hide_version |
/healthz returns only status and db without login |
all applies to all responses, site only to website pages (not /cms-admin, /api, /media, /_assets, /healthz). Invalid values: 400.
IllusionDB connections
Connections to IllusionDB servers for the workflow node illusiondb and idb.* in the function node. Stored in the instance (not in the bundle); workflows reference them by name.
GET /illusiondb ✏️ – [{ "name": "shop", "url": "https://db.firma.de", "has_token": true }] (the token is never returned).
PUT /illusiondb ✏️ – replaces the list: [{ "name": "shop", "url": "https://db.firma.de", "token": "…" }]. Name: letters, digits, spaces, . _ - (max. 64, unique regardless of case); address http(s)://… (an appended /api/v1 is removed). If token is missing (or null), the connection keeps its previous token – when renaming, matched via old_name; "" removes it. Response as for GET.
POST /illusiondb/{name}/test ✏️ – { "ok": true, "version": "1.4.0", "tables": 12 }, otherwise 502 with the message from IllusionDB (e.g. authentication required).
GET /illusiondb/{name}/tables ✏️ – readable tables of all data sources (without many-to-many junction tables, cached for one minute, ?fresh=1 reloads): [{ "id", "name", "schema", "source", "source_id", "ref", "public_read" }]. ref is the name as the node uses it (if ambiguous, Datenquelle/name).
GET /illusiondb/{name}/tables/{table} ✏️ – {table} as in the node (name, Datenquelle/name or ID): { "table": {…}, "columns": [{ "name", "ui_type", "field_kind", "primary_key", "nullable" }], "views": [{ "id", "name", "type", "default" }] }. Unknown table: 404.
Email sending
GET /system/mail ✏️ – SMTP access for the workflow node mail: { "host": "smtp.firma.de", "port": 587, "user": "web@firma.de", "from": "Website <web@firma.de>", "has_password": true } (the password is never returned).
PUT /system/mail ✏️ – same fields plus password; if password is missing, the previous one is kept. Port 465 = TLS, otherwise STARTTLS (user required). Stored in the instance, not in the bundle.
POST /system/mail/test ✏️ with { "to": "ich@firma.de" } sends a test email – 204, otherwise 502 with the server’s message.
Media storage
Where this instance stores its media files: locally (data/media, default) or in an S3-compatible storage (Garage, AWS, Hetzner, Cloudflare R2 …). Stored in the storage setting (not in the bundle). The URLs stay /media/… – the CMS delivers S3 files itself (range, 304), the bucket stays private. All endpoints also work with a token, i.e. via a connection in remote mode.
GET /storage ✏️
{
"type": "s3",
"s3": { "endpoint": "https://fsn1.your-objectstorage.com", "region": "fsn1", "bucket": "firma-media", "prefix": "cms/",
"access_key": "AKIA…", "path_style": false, "secret_set": true },
"cache_mb": 0,
"cache": { "enabled": true, "max_bytes": 536870912, "used_bytes": 10485760, "entries": 120, "default_bytes": 536870912 },
"media": { "files": 812, "bytes": 734003200 },
"migration": { "status": "idle" },
"encryption": "file"
}
The secret is never returned (secret_set). With type: "local", an entered S3 configuration is kept as a draft in s3. media counts the objects of the active storage (cached for one minute, partial: true after a 20 s time limit). encryption: key from IC_ENCRYPTION_KEY (env) or data/secret.key (file). If S3 could not be activated at startup (e.g. key changed), error/error_code are included and the local storage is active.
PUT /storage ✏️ – { "s3": { "endpoint", "region", "bucket", "prefix", "access_key", "secret_key", "path_style" }, "cache_mb": 0 }. If secret_key is missing (or empty), the stored one is kept – as long as access_key stays the same. If S3 is active, the new configuration is tested first and then applies immediately; bucket/prefix can then only be changed with a migration (409 storage.migrationRequired). With local storage it is just a draft; empty fields delete it. cache_mb (0 = start option -media-cache-mb) is the size of the disk cache in front of S3. Response like GET.
POST /storage/check ✏️ – { "s3": {…} } (without s3: the stored configuration) tests with a probe object (write, read, delete): { "ok": true }, otherwise 400 (configuration, e.g. storage.endpointInvalid) or 502 with storage.unreachable, storage.authFailed, storage.noBucket, storage.accessDenied or storage.checkFailed and the storage’s message in params.error.
POST /storage/migrate ✏️ – guided migration as a background job: { "target": "s3", "s3": {…}, "delete_old": false } or { "target": "local" }. Response 202 with the status. Steps (phase): check (test the target) → scan → copy (skips what already exists at the target with the same size and is not older) → verify (every copied object present at the target with the same size) → switch (save the setting, switch over, purge the page cache) → optionally cleanup (delete the old objects). Writes during the migration (uploads, deletions, bundle import) go to the old storage and are recorded; when switching they are paused briefly, the changed files are carried over, and afterwards they go to the target. Errors and cancellation leave the old storage active. 409 while a migration is running (storage.migrationRunning) or for the same target (storage.sameTarget).
GET /storage/migrate ✏️ – status:
{ "status": "running", "phase": "copy", "from": "local", "to": "s3", "delete_old": false,
"total_files": 812, "total_bytes": 734003200, "done_files": 300, "done_bytes": 201326592,
"skipped_files": 0, "verified_files": 0, "changed_files": 0, "deleted_files": 0, "started_at": 1791571753 }
status: idle, running, done, failed (with error, error_code, error_params), canceled. changed_files = changes carried over when switching, cleanup_errors = old objects that could not be deleted (warning only).
DELETE /storage/migrate ✏️ – cancels (until before switch; afterwards 409 storage.migrationTooLate, without a job 409 storage.migrationNotRunning).