Product

What’s new

Notable changes in recent IoTMan releases.

v0.9.1

June 2026

Cron jobs move to the Compute tab and run an existing atomic function.

Cron jobs reference a function

The Cron Jobs section now lives in the Compute tab, below your registered functions. Instead of writing a JSONata body inline, a cron job selects an existing atomic function to run on its schedule. If the workspace has no functions yet, the form points you to create one first.

Pause, resume, and delete cron jobs

Pausing, resuming, and deleting a cron job now work from the dashboard. The next-run time also displays correctly on each job row.

v0.9.0

June 2026

Atomic functions gain scheduling, email and FTP host functions, and outbound MQTT delivery.

Scheduled atomic functions

Atomic functions can now run on a schedule. Configure a cron job in the Process tab to fire a function at a set cadence, with no incoming event required.

Send email from a function

$send_email(to, subject, body) queues a message for delivery through your workspace's SMTP configuration. Delivery runs asynchronously with retry. SMTP is configured under Workspace settings.

Upload files over FTP

$ftp_upload(profile_id, remote_path, content) writes a file to an FTP server. Register reusable FTP connection profiles (host, port, username, auth) under Workspace settings and reference them by id.

Outbound MQTT delivery

A transformation output can publish its payload to external MQTT brokers on every match, alongside the existing webhook delivery. Configure brokers per endpoint in the Export tab.

Atomic function editor: test and highlight

The create and edit forms have a Test button that runs the function body inside a transaction and always rolls back, so you can dry-run without side effects. JSONata is now syntax-highlighted in the editor.

Views created by functions now backfill

A view created through $iotman_call from an atomic function previously stayed stuck materializing and returned 404 on public reads. These views now backfill existing history and go live correctly.

v0.8.2

May 2026

Event views materialize against existing source history on create and on PATCH.

Views backfill existing history

Creating a new view (or editing one via PATCH) now folds every existing event for its bound sources through the view's key and reducer before the view goes live. Previously a new view started empty and only saw events ingested after its creation. The view is hidden from public reads and skipped by the live ingest path until materialization completes, so each event is processed exactly once. The Compute tab shows a Materializing… badge on rows whose backfill is in flight and a banner above the views list that auto-dismisses when it's done; Edit and Delete are disabled during materialization. AsyncAPI spec import does not trigger backfill; spec-imported views land at done. Use the REST API on a view to trigger a fresh backfill.

Workspace scope grants take effect on the next request

Every scope check in iotman now reads the principal's current grants from the database (via an in-process cache) instead of trusting the scope claim embedded in the access token. Granting a new scope is visible to the next call; revoking a scope blocks the next call. Outstanding access tokens whose claim references a since-revoked scope stop working immediately; tokens missing a just-granted scope start working immediately. The OIDC scope claim is still populated on issued tokens so external resource servers that verify via JWKS continue to see it.

Cross-origin reads on /views and /functions

Browsers calling /views/<workspace>/<name>/data, /events, or /functions/<workspace>/<name> from a different origin previously failed CORS preflight (Caddy was routing OPTIONS to the static-file handler, which 404'd). Caddy now reverse-proxies these paths to the backend alongside /data. The backend has always responded correctly; the gap was at the front door.

v0.8.1

May 2026

Public reads on export endpoints come back; MCP OAuth discovery at canonical well-known paths.

Anonymous access on export endpoints

The Export tab has a new Anonymous access checkbox per endpoint. Off (the default) keeps the v0.8.0 behaviour of requiring a workspace Bearer token; on accepts unauthenticated GET, SSE, and WebSocket reads. The toggle locks off when a required scope is set so the two cannot be combined. Event views accept the same setting through the REST API and AsyncAPI import.

MCP OAuth discovery at the canonical RFC paths

MCP clients that look up OAuth discovery at /.well-known/oauth-protected-resource/mcp/{workspace} (and the matching auth-server and openid-configuration paths) now find the document. Previously they completed registration and then 404'd on re-validation. Existing clients using the per-workspace suffix path are unaffected.

Workspace settings invitation list

Pending invitations no longer appear twice while an invite is open.

v0.8.0

May 2026

New Compute tab: atomic functions and event views, with worker identities and SSE streaming.

Atomic functions

Workspace admins register a JSONata body at POST /functions/{workspace}/{name}. The body runs inside a Postgres transaction and can call back into any iotman admin endpoint via $iotman_call(method, path, body), authenticated as a Worker (an OAuth2 client flagged as worker identity). Useful for composing several admin calls into one atomic operation, such as provisioning an end-user, granting scopes, and creating an endpoint in one shot, or for adding a single piece of derived logic to the workspace's HTTP surface without standing up a separate service. The new Compute tab hosts the editor; JSONata is validated on debounce, with parse errors surfaced inline.

Event views

A view binds one or more sources, a JSONata key extractor, and a JSONata reducer. Each ingested event is keyed by the extractor (non-scalar results skip the view, so the extractor doubles as a per-event filter) and folded into a per-key row via the reducer. Public reads at /views/{workspace}/{name}/data serve point lookups and prefix scans; /views/{workspace}/{name}/events streams changes over SSE. Reads can be gated by a required scope and an optional row-scoping predicate, so each end-user only sees rows that match their identity. Canned reducers in the Compute tab cover last, first, count, and merge; custom JSONata is available for the rest.

Atomic-function triggers

A trigger fires a function automatically when a new event lands on a chosen source, with an optional JSONata trigger condition to filter. The function's reply, success or error, lands on a reply source you pick, so consumers subscribe to their own outcome stream instead of polling for state to converge. A reply-to-self shortcut routes the reply back to the originating source, removing the need to register N triggers (one per end-user source) just to vary the reply target.

Workers

OAuth2 clients can be flagged as worker identities in the Access tab. Atomic functions and views pick a worker on create or edit; the executor authenticates loopback calls as that worker, with only the scopes that worker holds. Scope grants and revocations take effect on the next invocation, with no token cache to wait out.

Server-Sent Events on export endpoints

Every Processed export endpoint now answers GET /data/{workspace}/{path}/events with Server-Sent Events. Frames match one item of the polled REST data array and the webhook POST body. Public endpoints stream immediately; protected endpoints require an Authorization: Bearer header (native browser EventSource cannot set headers, so browser callers use the eventsource npm polyfill). The existing WebSocket sibling at /stream still works but is now marked deprecated in its upgrade response and in the AsyncAPI document, and will be removed in a future release. New integrations should use SSE.

AsyncAPI now covers the Compute tab

The workspace AsyncAPI document gained sections for event views, atomic functions, and atomic-function triggers, under x-iotman extensions. Spec import round-trips these alongside the existing sources, exports, webhooks, MQTT configs, tag rules, and transformation outputs, so a workspace's AsyncAPI doc is once again a complete configuration substrate. HMAC secrets stay server-side, matching the existing webhook-secret behaviour.

Hosted MCP connectors that hold a client secret

Dynamic Client Registration now accepts confidential clients (token_endpoint_auth_method = client_secret_basic or client_secret_post), and /oauth2/token verifies the presented secret on the authorization_code and refresh_token grants. This unblocks hosted MCP connectors that store their credentials server-side, such as the Claude.ai web connector. Public PKCE clients (ChatGPT, Claude Code CLI) are unchanged.

iotman_call MCP tool

The per-workspace MCP server gained a generic-HTTP escape hatch: iotman_call(method, path, body, query, headers) sends an arbitrary HTTP request to the workspace's iotman surface using the caller's bearer. Useful for hitting routes that ship between MCP server restarts, the data-ingest path, OIDC introspection, anything else, without waiting for a per-endpoint tool. Path scope is locked to the workspace's own slug; method whitelist is GET, POST, PUT, PATCH, DELETE; response body is capped at 256 KB. Mirrors the $iotman_call host helper available inside atomic-function bodies.

Managed endpoints require an authenticated token

Export endpoints, event views, and atomic-function invokes now require an Authorization: Bearer for the workspace, even when no scope is set. The audience claim must match the workspace slug. Previously, an export endpoint or view with no required scope was treated as fully public, which was rarely the intent. Only HTTP source endpoints you create remain legitimately publishable as public. Existing protected endpoints are unaffected.

v0.7.0

May 2026

AsyncAPI export and import, per-source schema validation, BYO workspace SMTP, third-party OTP sign-in.

AsyncAPI 3.0 for the whole workspace

Each workspace now publishes a single AsyncAPI 3.0 document covering its HTTP sources, MQTT sources, export endpoints, and webhook subscriptions, plus tag rules and transformation outputs under x-iotman-* extensions. Download asyncapi.yaml or asyncapi.json from the new AsyncAPI panel in workspace settings, edit in your tool of choice, then import with a dry-run diff before applying. An Admin-only Public toggle exposes the document without authentication so external tooling can fetch it directly.

Per-source JSON Schema validation

Every HTTP endpoint row in Connect has a Schema button. Paste a JSON Schema, pick a validation mode (off, warn, or reject), and ingest checks each payload against it. Reject returns 422; warn accepts the payload but echoes the violations back to the producer in the response body and an X-IotMan-Schema-Violations header, so a producer can see and fix its own bug without an operator opening the dashboard. MQTT ingest honors the same schema and modes.

Bring your own SMTP per workspace

A new Workspace SMTP card in workspace settings stores per-workspace SMTP credentials and routes that workspace's OTP and invitation emails through them. Passwords are stored encrypted and never returned from the dashboard. A Send test email panel validates credentials before you commit. When no workspace SMTP is configured, sends fall back to the platform mailer. An Email deliveries card below the form shows the last 50 sends with status and error inline, refreshing automatically after each test.

Workspace member invitations actually work

The Invite member form on workspace settings sends an email with an accept link, lists pending invitations with a cancel button, and routes the invitee through the login flow if they aren't signed in yet. Accept is bound to the email the invitation was issued to; signing in as a different account returns a clear error.

OTP sign-in for third-party apps

A new OAuth2 grant lets third-party apps authenticate users against IoTMan without redirecting the browser to iotman.io. POST /api/auth/send-otp with the app's client_id mails a 6-digit code (the email names the requesting app); the app then exchanges the code at /oauth2/token using grant_type=urn:iotman:params:oauth:grant-type:otp. The code is bound to the calling client at issuance, so it cannot be redeemed at another client. /api/auth/send-otp accepts cross-origin requests; the rest of /api/auth/* stays same-origin.

Workspace URLs use the slug everywhere

The workspace identifier in every URL is now the slug: /api/workspaces/{slug}/..., /data/{slug}/{url-path}, and /mcp/{slug}. The OAuth2 aud claim and TokenInfoResponse.workspace_id now also carry the slug. Existing slugs are preserved verbatim, so existing devices, AsyncAPI consumers, and bookmarks continue to work. Access tokens minted before the cutover are invalidated and need to be refreshed; refresh tokens, cookie sessions, and dashboard logins are unaffected.

MCP improvements

The per-workspace MCP catalog now ships with instructions pointing agents at the AsyncAPI bulk-config path so they can read or rewrite the workspace in one call instead of stepping through dozens of CRUD tools. A ?mode=lean query parameter trims the catalog to GET-only tools plus the AsyncAPI tools. Workspace settings shows the MCP endpoint URL with a copy button. Hosted MCP clients (ChatGPT, web Claude) that register through Dynamic Client Registration can now complete sign-in against their own callback URL instead of being forced onto loopback.

Discovery surfaces for crawlers and agents

The OpenAPI spec is now served at /openapi.yaml. /llms.txt indexes the marketing and docs pages for AI agents; /llms-full.txt concatenates the full docs corpus in one fetch. sitemap.xml and robots.txt cover search crawlers. Open Graph and JSON-LD metadata are set on every page so link previews resolve consistently.

New landing and IoT vertical page

iotman.io now leads with a managed-backend framing aimed at small product teams; the IoT-first landing moves to /solutions/iot and remains linked from the nav and footer. The dashboard demo gets its own slot in the nav next to Login.

v0.6.0

May 2026

Raw and Processed endpoints unified; constructors gain $received_at and $metadata; MCP live in prod.

One endpoint kind

The split between Raw and Processed endpoints is gone. Toggling Read access on an HTTP endpoint in Connect now creates a Processed export endpoint pointing at a passthrough transformation, so REST, WebSocket, and webhooks all work uniformly on every endpoint listed under Export.

$received_at and $metadata in constructors

Two real PostgreSQL JSONPath named variables are bound on every constructor evaluation. $received_at is the row's ingestion timestamp; $metadata is the per-row ingest metadata (source_ip, auth_method, oauth2_client_id, ingest_context, …) and is fully subscriptable. Use them as values, in subscriptions like $metadata.source_ip, or inside filter expressions: $.value ? ($metadata.auth_method == "oauth2").

Soft-validation on output save

Saving an output now runs the constructor through a synthetic-row evaluation before persisting. Typos like $recieved_at fail at save time with a 400, instead of silently emitting nulls when real rows are processed.

Webhooks visible on every endpoint

Because every endpoint is now Processed, the Webhooks block in the expanded Export-tab row renders for every endpoint — including the ones materialised by the Connect-tab Read-access toggle.

MCP server live in prod

The MCP endpoint announced in v0.5.0 is now actually serving traffic. /mcp/{workspace-uuid} and the OAuth2 discovery endpoints under /.well-known/ respond instead of returning 502.

Sign-in confirmation replaces the consent screen

The OAuth2 consent step is now a plain "Sign in to {App} as {email}?" prompt with a "Use a different account" escape hatch. Scope strings are gone from the page — admins decide what an end-user can do, so listing them on consent informed nobody.

Admin grants and revocations take effect immediately

Internal API Bearer requests now check the principal's iotman: scopes against the database on every request. Granting or revoking a scope from the Access tab no longer waits for the access-token TTL to roll over.

Webhook signing secrets recoverable from the dashboard

Webhook signing secrets are shown in full on every read, not just on creation. The Secret column on the Webhooks block has a copy button and survives lost dashboard sessions; you no longer have to re-create a webhook to recover its secret.

iotman: client tokens keep their grants

Authorization-code tokens issued to third-party clients now always include the iotman: scopes the user was granted, instead of being narrowed away by the client's requested-scope list. Matches how refresh-token rotation already worked.

v0.5.0

April 2026

Webhooks, MCP server, programmatic dashboard access, per-workspace data retention.

Webhooks for Processed exports

Each Processed export endpoint accepts webhook subscriptions. Deliveries are signed with HMAC-SHA256 over a timestamp and body, retried on a 1m / 5m / 30m / 2h / 12h schedule, and deduped on event id. The signing secret is shown once on creation; the dashboard shows last-delivery status and circuit state per subscription.

WebSocket streaming on Processed endpoints

Processed export endpoints now accept a WebSocket connection in addition to REST GET. Frames carry the transformed payload — the same shape as one item of the polled data array and the webhook POST body. Live-only; REST covers backfill.

MCP server

Each workspace exposes an MCP endpoint at /mcp/{workspace-uuid}. Every iotman API operation is available as an MCP tool. Authentication uses your existing OAuth2 client. Claude Code, MCP Inspector, and other spec-compliant clients can register dynamically and obtain a token via PKCE.

Programmatic dashboard access

Internal API routes under /api/workspaces/{id}/... now accept OAuth2 Bearer tokens in addition to the dashboard session cookie. PKCE end-users and Client Credentials tokens can manage workspaces directly. Scopes are granted per email (end-users) or per client (Client Credentials) from the Access tab.

Granular Internal API scopes

Internal API permissions now use a per-resource, per-verb scope namespace (e.g. iotman:endpoints:create, iotman:webhooks:read). Built-in Admin / Editor / Viewer roles expand to the equivalent granular sets. The Access tab exposes Read-only / Operator / Full access preset chips for one-click bundles, with per-scope add and revoke for fine-tuning.

Per-workspace data retention

Each workspace now has a retention setting in days. Records older than the cutoff are dropped automatically. Leave it unset to keep data forever.

API reference page

The full OpenAPI spec is published at /openapi.yaml and rendered as a browsable reference at /api-reference.

Tag Rules consolidated

Tag categories and dynamic rules are now one concept. The Process tab edits both the rule fields and the tag metadata in a single form. The /tag-categories and /dynamic-rules API paths are removed; use /tag-rules.

Legacy data URLs removed

The token-based /data/{endpoint_token} ingest, read, and stream URLs and the UUID-keyed /data/out/{id} export URL are gone. Use the workspace-scoped /data/{workspace-short-id}/{url-path} URL shown on the Connect and Export tabs. The Deprecation headers shipped in v0.4.0 already pointed at the replacement.

v0.4.0

April 2026

Ingestion auth reworked, workspace-scoped export URLs, WebSocket streaming.

Raw and Processed output endpoints

Output endpoints are now classified as Raw or Processed. Raw endpoints deliver each record exactly as it was ingested — same URL as the input, only the method differs. Processed endpoints run a constructor to shape the payload. Raw skips the transformation step, so polled reads are cheaper and WebSocket streaming is available. WebSocket is not yet available on Processed endpoints.

Workspace-scoped export URLs

Export endpoints are served at GET /data/{workspace-short-id}/{url-path}. Both segments are set per workspace and per endpoint.

Per-endpoint WebSocket streaming

Raw endpoints accept a WebSocket connection and push new records as they are ingested. Each endpoint's stream is scoped to its own source; earlier releases sent every workspace event to every stream subscriber.

Token-free WebSocket URLs

Protected streams authenticate via a first-message handshake instead of a query-string token. The access token no longer appears in URLs or server logs.

Ingestion auth: OAuth2 only

Basic-auth and mTLS are removed. Each ingestion endpoint is either open or protected by OAuth2; protected endpoints validate a Bearer access token against a required scope.

OIDC userinfo replaces introspection

GET /oauth2/userinfo returns the standard OIDC claims. The previous introspection endpoint is removed.

Per-record ingest context

Each stored record now carries a metadata object captured at ingest time (such as the token that wrote it), queryable alongside the payload.

30-day session cookies

Dashboard sessions now last 30 days. Sign-in is still passwordless.

Privacy policy and cookie disclosures

A privacy page is now published at /privacy, listing cookies and the legal basis for processing.

This page

Notable changes are now summarised here on every release. v0.4.0 is the first entry; earlier releases are not backfilled.