Currently running v0.2.232 · 545b830 · RSS feed
v0.2.230
Added
- A workspace-wide request log at
/logs, in the sidebar. It shows every public API request across all your projects, with a project column and the same filters as the per-project log. The dashboard traffic and error cards now link straight to it instead of asking you to pick a project first.
v0.2.229
Fixed
- A payment-processor error no longer breaks the page. Opening the billing portal or starting an upgrade would show "Something went wrong on this page" if the request to Stripe failed for any reason. Both now explain what happened and leave you where you were.
- A clearer message when a workspace's billing record is stale, instead of advice to try again that could never work.
Added
- Members is now in the sidebar, next to Plans and the audit log. Inviting and removing people was only reachable through the workspace dropdown.
v0.2.228
Changed
- The audit log is now its own item in the sidebar, at
/audit, next to Plans and Settings, on desktop and in the mobile menu. It was only reachable through a button inside settings.
v0.2.227
Added
- The audit log has its own page. It was a 50-row filterable log sitting inside account settings, competing for space with your account details, plan limits and agent tokens. Settings now shows the last five with a link to the full log, which keeps its filters, date range and CSV export.
- An "Errors" option in the request log filter, covering 4xx and 5xx together. The dashboard's error card counts both, and until now no single filter matched it, so the link from that card could only show you more or less than the card itself.
v0.2.226
Fixed
- A revoked agent token no longer keeps showing its value. Revoking a token you had just created left its text on screen, so a dead credential looked like a live one.
- Dashboard traffic and error entries now show the date, in your own timezone. They showed a bare clock time in UTC, so two entries a day apart were indistinguishable.
Added
- A link from the dashboard traffic and error cards to the full log.
v0.2.225
Changed
- Every "shown once" secret now appears without reloading the page. Creating a workspace invite, generating a consumer share link, or rotating your API key from the consumer portal used to reload the whole page and drop you back at the top, often well above the thing you had just created. The value now appears in place. It is still shown only once.
- The rotated key appears on the key you rotated. The portal previously assumed the new key was the first in the list, so with several keys the reveal could attach to the wrong row.
v0.2.224
Added
- Rotate an agent token. If you lose a token, you can now replace it in one click: the new one is shown once and the old one stops working immediately. It keeps the same name and the same permissions, so rotating never quietly widens what a token can do. There is deliberately no way to view an existing token, because only a hash of it is stored.
Changed
- Creating a token is instant and no longer jumps the page. It used to reload the whole settings page and drop you back at the top, several screens above the token you had just created. The token now appears in place.
v0.2.223
Changed
- The account settings page loads several seconds faster. It was running eight database queries one after another when most of them do not depend on each other. They now run in two parallel waves. This is what made the page sit blank after creating an agent token.
v0.2.222
Fixed
- Creating a management token no longer breaks the settings page. Showing a freshly-created secret crashed the page it was supposed to appear on, and kept crashing on reload for about a minute afterwards. The same fault affected every "shown once" secret in the product: workspace invite links, consumer portal share links, and rotated consumer API keys. All four now display correctly, and still only once.
v0.2.220
Fixed
- Plan limits are now resolved everywhere they are enforced. Four paths decided your caps from a copy of your plan stored on the project rather than from your workspace's actual plan: creating an API key or a resource over the internal API, creating a resource over the management API (the one AI agents call), and importing a resource from CSV. Because that copy only stores a plan name, any admin-granted allowance above your plan was dropped, and the same action could be allowed in the dashboard and refused over the API.
- A repair for projects left on an old cap. Upgrading a workspace used to leave existing projects at the request limit they were created with, which was fixed in 0.2.216 for future upgrades only. There is now an operator command that finds and corrects projects still carrying a stale limit.
- Editing a record can no longer create a duplicate of a unique field. Two updates arriving at the same moment, both setting the same unique field to the same value, could both succeed and leave two records holding it. Creating records was already protected against this; editing them now is too.
- Deleting a reading that does not exist returns 404. It returned 400, which is the code for a malformed request and is not in the documented list for that endpoint, so clients following the usual "retry on 404, do not retry on 400" rule behaved incorrectly.
Changed
- Faster authenticated API responses. Each request wrote a usage record that duplicated data already captured in two other places. It has been removed, and the remaining bookkeeping now happens after the response is sent instead of before. Your usage numbers are unchanged: they are read from the same counters your limits are enforced against.
- Your account's monthly request total now matches the workspace you are viewing. It previously added up every workspace you belong to, while the resource and API-key figures beside it counted only the current one.
Operational
- The daily billing sweep is bounded. It reported usage to Stripe one workspace at a time with no limit on how many per run, inside a request that also runs four other jobs. It now runs a bounded number in parallel, caps how many it bills per run, and reports anything it deferred to the next run.
- Reading an ingested stream no longer writes on every request. A one-time milestone was being re-written on every read for the life of the project.
v0.2.219
2026-08-14Fixed
- A blank screenful of page, on every page. An empty overlay container was taking up a full screen of space at the bottom of the document. Following a link within the site then landed you on the next page with that dead space pushed above the content, so the page opened scrolled past its own heading. Desktop and mobile both.
- The bottom bar no longer covers the end of the page on a phone. Scrolling to the bottom of a dashboard page left the footer underneath the fixed navigation bar. The clearance now sits on the whole page instead of just the content area above the footer.
v0.2.218
Fixed
- Your dashboard and your customer's portal now show the same usage number. The consumer portal read the counter your quota is enforced against, while the dashboard aggregated raw telemetry. Those diverge whenever a request fails after being counted, so the two surfaces could disagree about the same consumer in the same month with nothing to say which was right. The dashboard now reads the same counter the 429 is decided from.
v0.2.217
Added
- The ingest API is now version pinned, at
/api/ingest/v1/.... The docs, the OpenAPI document and the generated snippets all emit the versioned form, and the document states plainly that a breaking change will add a version besidev1rather than changingv1. - The unversioned paths keep working, indefinitely. If you have devices posting to
/api/ingest/{username}/{project}/ingesttoday, nothing changes and nothing needs reflashing. Both URLs run the same code.
v0.2.216
Fixed
- Upgrading now actually raises your request limit. A project's monthly request cap was written once, when the project was created, and never updated again. So a workspace that upgraded from Free to Pro kept every existing project capped at 10,000 requests a month, and kept getting 429s there, while being billed for Pro. Only projects created after the upgrade got what was paid for. Caps now follow the plan in both directions, and admin overrides reach them too.
- Plan limits are no longer read from the free plan on every tier. Publishing a stream, creating a resource, and creating an API key each checked the free plan's allowance regardless of what you pay for, so a paying customer was refused at the free tier's number. Two of those messages also named a limit the plan does not set ("3 resources" against a limit of 5, "1 API key" against a limit of 2). All three now resolve your real plan and state its real number.
- A leaked webhook signing secret can be rotated. The capability existed but had no control anywhere in the dashboard, so the only recovery was deleting the endpoint and re-registering it at the receiver. Each endpoint now has a Rotate secret button. The new secret is shown once and the old one stops working immediately, which the confirmation says before you click.
- Consumer plans can be edited. You could create and delete a tier but not change one, so fixing a price meant deleting the plan and unassigning every consumer on it. Plans are now editable in place.
- Concurrent readings no longer lose a detected field. When two readings for the same stream arrived at once, each merged its new field into the schema independently and whichever finished second overwrote the other, so a field could be detected and then silently disappear.
- BigQuery discovery no longer stalls or silently truncates. It listed tables one dataset at a time with no limit, so an account with many datasets could time out before returning anything. It also never followed pagination, so a large account saw an incomplete list with no indication. Listings now run in parallel with a cap, follow pagination, and say plainly when a tree is too large to show in full.
v0.2.215
Changed
- Batch ingest is no longer slower the bigger the batch. A request carrying 500 readings opened 500 separate database transactions, one per reading, and held a connection for all of them. It now writes the whole batch in one. Measured locally on 500 readings: 500 transactions and 730 ms before, 1 transaction and 448 ms after, and the transaction count no longer grows with the batch at all.
- Nothing about the result changes. Every reading is still written, each still gets its own id, and a device's reading count and last-seen time still move with the readings that belong to it. One malformed reading still fails on its own without costing you the rest of the batch.
v0.2.214
Added
- The ingest pipeline now publishes its own OpenAPI document, at
/api/ingest/{username}/{projectSlug}/openapi.json. Every endpoint you use to get data in and inspect it (ingest, data, devices, schema, alerts, debug, metrics, sandbox) is now machine readable, so you can point Postman, an SDK generator, or an AI tool at it instead of reading the docs page. The URL is on the Ingest tab next to the endpoint list. - It describes your project's own field names, following the vocabulary you ingest under, and it says plainly that it is not version pinned. The published resource API remains the versioned contract, and the document links to it.
v0.2.212
Added
- Ingest readings now have a retention window, stated on every plan. Free keeps 30 days of history, Starter 90, Pro 180, Team 1 year, Business 2 years, Enterprise 3. Readings past the window are removed by the daily retention sweep. Until now there was no limit at all and nothing ever deleted a reading, which meant the pricing page quietly promised unlimited storage on every tier including Free.
- Open alerts can be resolved from the Ingest tab. The alerts list could only grow: schema-drift alerts cleared themselves when you re-synced, but an anomaly alert had no way out. Each alert now has a Resolve button.
Fixed
- A device can no longer overwrite the id of its own reading. A payload containing a field named
idreplaced the record id EndpointOS assigns, so a consumer using that id to fetch the reading back addressed something that did not exist. The platform's id now wins, and the device's value is kept asraw_idrather than discarded.recorded_atandissuesare protected the same way. - The error you get when deleting a reading now points at something real. It told you to use project data retention, which did not cover readings. It does now, so the advice is true.
v0.2.211
Fixed
- Listing readings no longer slows down as your data grows. The unfiltered
GET /datalist and the dashboard's recent-readings panel were scanning every reading in the project and sorting the result. On 200,000 readings that measured 41.9 ms; it is now 0.026 ms and flat. - Two alerts for the same condition can no longer appear. Two readings arriving at once could each detect the same schema drift on the same stream and each raise an alert, so you saw the same problem listed twice. The database now enforces one open alert per stream and kind, which is what the alerts panel always claimed to show. Existing duplicates are resolved automatically, keeping the most recent one.
- Raising an alert no longer scans the alert history. On a stream with a lot of past alerts this ran a full scan before every single alert write.
v0.2.210
Fixed
- Ingest projects outside IoT now publish their own field names. A commerce project's API returns
order_idandrecord_type, but its generated contract, OpenAPI, SDK and docs describeddevice_idanddevice_type, inventing four fields that never existed and omitting two that did. The same applied to events, metrics and entities. Every one of those surfaces now follows the vocabulary the project actually ingests under. If you publish a stream on one of these domains, re-sync it from the Ingest tab to pick up the corrected field list. - A field the API returns is no longer missing from the contract. When two payload fields resolve to the same normalised name (a generic
valuecarrying a unit alongside a literaltemperature, say), the loser is kept under araw_prefix. The API always returned it; the published contract never declared it, so it was invisible to OpenAPI, the SDK and the docs.
Changed
- Ingest writes roughly half as much per reading. The time-to-first-value milestone was recorded on every reading, costing two database writes each to record something that happens once per stream. A 500-reading batch paid 1000 of them. It is now written when a stream is first seen, which is the only moment it can change.
Security
- A device payload can no longer overwrite the fields the platform guarantees. A payload containing a key named
source_typeordevice_typereplaced the platform's own value with whatever the device sent.device_typedecides which published resource a reading belongs to, so a spoofed value made the response body disagree with what the platform filters on. Those keys are now reserved, and a payload field that lands on one is preserved under araw_prefix instead. Nothing is dropped.
v0.2.209
2026-08-14Added
- Your agent can now evolve an API you already shipped, not just create one. Ask for a new field ("add a shipped_at timestamp to orders") and it gets added. It lands on your draft, never published: publishing stays a button you press. Renaming, retyping and removing a field are deliberately not possible this way, because each breaks existing consumers.
- Ask "is anything about to break?" before you publish. The agent reads your contract diff and tells you which changes are safe and which are not.
- The management API now publishes its own OpenAPI document at
/api/manage/v1/openapi.json, readable without a token. If you would rather script against it than use an AI tool, that document is the contract.
Changed
- Issuing production API keys is now opt-in per agent token. Tick "Allow production keys" when creating a token if you want that agent provisioning live credentials. Existing behaviour otherwise: agents create development keys. Asking for a production key without the option is refused outright rather than quietly answered with a development key.
Fixed
- Firestore: comparison filters work. A query like
price[gte]=1000returned a server error, because Firestore requires results ordered by the compared field and we did not do that. It now works without you sending a sort. The two shapes Firestore genuinely cannot run (sorting by a different field, or comparing two different fields) now return a clear 400 naming the conflict, with troubleshooting entries in the guide. - BigQuery: moving a resource to a dataset in another region works. Editing the dataset kept the old region, so every request failed with "not found in location US" and recreating the resource was the only way out. The region is now re-resolved when you repoint it.
Security
- Freshly created secrets can no longer appear on the wrong page. Rotating a consumer's API key and then opening a different consumer's page within a minute could show the first consumer's new key. The same applied to portal share links and, briefly, agent tokens across workspaces. Every one-time secret reveal is now bound to the thing it belongs to.
v0.2.208
2026-08-14Added
- AI coding tools can now build and evolve your APIs for you. Issue an agent token under Settings, paste one config line into Claude Code or Codex, and ask for what you want: "create an API for orders with id, customer email, total and status". The agent creates the project, defines the resource, issues a development key, and hands back the base URL, the docs link and a working curl. No browser required.
- A management API you can drive yourself. The same surface is a plain documented HTTP API at
/api/manage/v1, so anything that speaks HTTP can use it whether or not it speaks MCP. - A new Agents topic in the docs covering how to issue a token, the config block to paste, and each use case with real requests and responses.
Security
- An agent token cannot delete anything. There is no delete capability and no delete endpoint on this surface, so destruction is not expressible through it rather than merely refused.
- A token is scoped to one workspace and cannot see another, even if you belong to several.
- It cannot read back an API key that already exists, and it cannot publish a contract version. Publishing stays a human decision.
- Keys an agent issues are development keys unless production is asked for explicitly. Every change an agent makes is written to the audit log, and a token can be revoked in one click.
v0.2.207
Security
- Error messages from Supabase no longer appear in the page address after a failed MFA action. The wording shown is now written by us, and the underlying detail goes to the server log instead of the address bar, where it would reach browser history and logs.
v0.2.206
Changed
pagination.nextCursoris now in the OpenAPI document and the generated TypeScript SDK. The field has been returned since 0.2.182 for external Postgres and BigQuery resources, but it was never declared, so anyone generating a client from the published contract had no way to know it existed.- Asking for an
offsetabove 100,000 now returns a clear 400 instead of quietly giving you the wrong page. Previously the request was folded down to the cap and answered 200, so a client paging a large table could silently repeat or skip records. The limit is now stated in the API document too. Usecursorpaging to go beyond it. - Sending both
offsetandcursornow returns a 400. The API document already said the two cannot be combined; the server was silently ignoringoffsetinstead of saying so.
v0.2.205
Operational
- The daily log-retention job now looks up every workspace's plan in two queries instead of two per workspace. On accounts with many workspaces this ran hundreds of queries before deleting anything. What gets deleted is unchanged.
- The over-quota check does one fewer database join. It does not change any quota decision.
v0.2.204
Fixed
- The search button is visible again in the dashboard header on a phone. The workspace switcher was overrunning its space and covering it, so search looked missing and could not be tapped. The workspace name now shortens to fit instead.
v0.2.203
Fixed
- Sign out from the command palette now actually signs you out. Confirming it opened a new tab to a page that does not exist and left you logged in.
Operational
- The "last used" timestamp on an API key is now written at most once a minute per key instead of on every single authenticated request. The timestamp is unchanged in what it shows; this only removes a database write per request.
v0.2.201
Security
- Connector health-check failures no longer put the raw driver error in the page URL. A failing check could place connection details from the underlying database or API into the address bar, where they reach browser history and server logs. The message shown is now cleaned the same way the stored copy already was.
Fixed
- Resource, consumer and API key names in a list are now big enough to tap on a phone. They were 20px targets.
- The API keys attached to a consumer are readable on a phone. The four-column table now stacks into cards on small screens.
- Request log filters no longer push the logs off the screen on a phone. Six filters used to fill the display, so the logs they filter started below the fold. They now sit behind a Filters button that shows how many are active.
- Versions is now in the project navigation. The page existed but could only be reached by typing its address.
v0.2.200
Fixed
- One close button on the mobile menu instead of two overlapping ones. The menu's own close control and the header's toggle sat on the same spot on the pricing, docs and legal pages, and the wrong one was on top.
v0.2.199
Fixed
- The menu on the marketing pages now opens as a proper full-height panel with its own close button. The previous release moved it out of the header so it would stop being clipped, which fixed the clipping but left it unstyled and without a visible way to close it.
v0.2.198
Fixed
- The menu on the marketing pages opens properly on a phone. Tapping the hamburger painted a small panel clipped to the top strip of the screen instead of sliding out a full-height menu, so most of it was unreachable.
- The workspace switcher works on a phone. Its panel was anchored to the switcher chip at a fixed width, which put most of it past the right edge of a phone screen. It now sits within the screen at any size, unchanged on desktop.
- Project cards no longer run off the side of the screen. A long base URL could stretch a card wider than the space it sits in, pushing the plan badge out of view.
Operational
- Layout checks now also catch content that is wider than the box holding it, not only pages that scroll sideways. The project card fault was invisible to the old check because a parent clipped the card rather than the page scrolling.
v0.2.197
2026-08-13Changed
- The dashboard has real navigation on a phone. Getting around used to mean swiping two hidden side-scrolling strips: of the fifteen places you can go, only five were visible, and the rest sat behind up to 683px of sideways scrolling with no scrollbar to hint they existed. There is now a fixed bar at the bottom of the screen with the surfaces you check most (project overview, logs, insights), and a More button that opens everything else. Every destination is reachable in at most two taps.
- On a tablet, the navigation tabs now wrap onto a second row instead of scrolling sideways, so none of them are hidden. Desktop is unchanged.
Fixed
- Buttons and links are big enough to hit on a phone. Every button size now meets the 44px minimum on small screens while keeping its compact size on desktop. This covers the copy buttons, the "create your first ..." actions on empty pages, the workspace switcher, resource links, and the sort control on the projects list.
- Text you copy no longer misses. The copy button next to base URLs, keys and endpoints was 32px tall on every screen.
Operational
- The dashboard is now checked in a real browser at phone width on every change: no page may scroll sideways, every navigation destination must stay reachable in two taps, and no control may fall under the 44px minimum.
v0.2.196
Fixed
- Two updates arriving at once no longer lose one of the edits. Updating a record on EndpointOS-managed storage read the stored document, merged your fields in, and wrote the whole thing back. If a second update landed in between, it had already read the older document, so its write undid the first change. Both requests returned success and one edit was silently gone. The merge now happens in a single database operation, so concurrent updates to different fields both stick.
Operational
- Updating a managed record does one less database query. Nothing about the response changes.
v0.2.195
Security
- Changing your password now requires your second factor. If you have two-factor authentication on, every other sensitive action on your account already asked for it, but the password form did not. Signing in with a correct password puts a session at the first factor only, and from there the password could be overwritten without the second one ever being presented. That could not read any of your data (the dashboard still held the line, and changing a password does not remove your authenticator) but it could lock you out of your own account. It now asks.
- This applies to the password-reset email too. Following a reset link asks for your authenticator code before you set the new password, so access to your inbox alone is no longer enough to take over an account with two-factor turned on. If you have lost both your password and your device, you are in exactly the position you were before: contact support. Verifying your code now returns you to the form you started on rather than dropping you on the dashboard.
Fixed
- A freshly created workspace invitation link now only appears on the workspace it belongs to. Creating an invite and then switching workspaces within a minute could show the new link on the wrong workspace's members page. Worse, that stray view consumed the one-time link, so the page meant to show it showed nothing and the invitation had to be created again. The link now stays put until the right page displays it.
v0.2.194
Changed
- The generated TypeScript SDK now types the details on its newer errors.
error.detailsonly described rate limits, so checking why a data source refused a query, or which write it cannot perform, gave youunknownand needed a cast.reason,supported,operation,allowandfieldare now typed. This is additive:detailsstill allows any other key, so code already reading one keeps working. isRateLimited()no longer describes itself as monthly-only. It returns true for the monthly project and consumer caps *and* the per-minute API-key and IP limits, which is what it always did.error.details.scopetells you which one you hit.
Fixed
- A query rejected because a data source cannot paginate the way you asked reports
paginationas its reason. That reason was always sent, but was missing from the documented list, so it read as undocumented behaviour.
v0.2.193
Fixed
- A unique field holding JSON no longer rejects values that merely overlap an existing one. Uniqueness was checked with a "contains" comparison, which is the same thing as equality for text and numbers but not for JSON: storing
{"a": 1, "b": 2}made a later{"a": 1}look like a duplicate, so a legitimately different value was refused. JSON fields are now compared for exact equality, and key order does not matter.
Operational
- Creating a record with a unique field is faster. The check that runs while the write holds its lock was not using the index built for it, so every such create did a full scan of the resource's records at the moment it was least affordable. Both the fast pre-check and the authoritative one now share a single query, which also means they cannot disagree about what counts as a duplicate.
v0.2.192
2026-07-31Fixed
- An archived or paused project's schema and SDK are no longer downloadable. Stopping a project already hid its documentation, its changelog and its live schema, but a project that had published an API contract kept serving that contract's OpenAPI document and its generated TypeScript SDK on four URLs, both the current ones and the version-pinned ones. All four now return the same 404 a project that never existed returns. The data endpoints are unchanged and still answer 503, so a consumer can still tell a paused project apart from a deleted one.
- A request body over the size limit now returns
PAYLOAD_TOO_LARGEinstead ofVALIDATION_ERROR. This is a new error code.VALIDATION_ERRORmeans 400 everywhere else in the API, so reusing it for the only path that answers 413 meant code alone was not enough to tell what happened. If you branch onerror.codefor oversized bodies, switch toPAYLOAD_TOO_LARGE; the status stays 413. The OpenAPI document now also spells out both codes that can arrive with a 413 on a write, since a storage cap and an oversized body need different responses from you. - The generated TypeScript SDK types each resource against its own schema. Two resources in one project whose names reduce to the same singular form (
carandcars, for instance) both got typed as the first one, so the second resource'slist(),get()anddelete()returned a type describing the wrong fields. The SDK now reads the type from the contract rather than inferring it from the resource name. - The SDK's error type no longer denies a real value.
error.details.scopeis typed for autocomplete, and the type listed only the rate-limit values, so narrowing on it told you a workspace storage cap could not happen.workspaceis now included, and the type is generated from the same list the API uses.
v0.2.191
2026-07-30Fixed
- A read-only data source no longer advertises endpoints it cannot serve. A BigQuery resource published Create, Update and Delete endpoints in its OpenAPI document and its generated docs, complete with request schemas, because the access mode defaults to read/write for every source and nothing checked whether the source could actually write. Sending one of those requests returned a 500. The published contract now shows only what the source implements, and creating a resource on a read-only source stores it as read-only.
- A write to a source that cannot write returns 405
WRITE_NOT_SUPPORTEDwith anAllowheader listing the methods that do work, instead of a 500 with an internal message. This is a new error code: it is separate from 403, which means the project owner locked the resource, because a locked resource can be unlocked and a read-only data source cannot start writing.error.details.operationnames the refused write anderror.details.supportedlists the ones the source does implement. - The generated TypeScript SDK now describes each resource's real query support. Every
list()method carried the same comment naminglimit,offset,q,sortand field filters, which on a BigQuery resource pointed at two parameters the API rejects. The comment is now built from that resource's own contract.
Changed
- Ask AI answers link straight to the documentation page for a topic rather than to the index with an anchor, and the answer about data sources now says that query support differs per source.
v0.2.190
Fixed
- You can now move between documentation pages without going back to the index. Every reference page and setup guide has a sidebar listing all the others, with the current one marked, so any page is one click from any other. Splitting the reference into separate pages had left each one with a single unstyled link out and a pair of plain text links buried at the bottom.
- Previous and next are now visible cards at the end of a page rather than a line of small text, and there is a breadcrumb at the top showing where you are.
- Each topic name appears once. Every reference page was showing its title twice, once from the page header and again from the section beneath it.
- The navigation collapses into a scrollable strip on narrow screens rather than pushing the content down the page.
v0.2.189
Changed
- The data-source setup guides are now part of the documentation navigation rather than a separate list. Each guide sits under the "Connect your data" topic, and the topic, the guides and the sitemap all read one list, so adding a data source brings its guide into the docs automatically.
v0.2.188
Fixed
- A range filter on a Supabase resource returned the wrong rows. Asking for
price[gte]=30000&price[lte]=45000applied only the upper bound, so you got everything below 45000 including rows under 30000. Two conditions on the same column are now combined the way PostgREST actually requires. This shipped in 0.2.177 and was found by running the connector against a real Supabase for the first time. - Filter values containing a comma, quote or bracket are quoted correctly, so a value like
Rolls, Royceis one value rather than two. - A Supabase table created with SQL rather than through the dashboard returns "permission denied" until the service role is granted access. The connector guide now explains that, with the exact grant, because row-level security and table privileges are separate things and the service-role key bypasses only the first.
v0.2.187
Fixed
- All nineteen documentation pages are now in the sitemap. Splitting the reference into a page per topic left the twelve topic pages and the seven data-source guides invisible to search engines, because listing
/docsused to cover everything. The sitemap is now generated from the same lists the pages are, so a new topic or a new data source cannot be added and then quietly left out.
v0.2.186
Changed
- Every documentation topic now has its own page. The reference was twelve topics sharing one 862-line page, so the only way to link to "API keys" was an anchor two thirds of the way down a wall of text. Each topic is now a page you can send someone:
/docs/api-keys,/docs/limits, and so on. The docs home is a hub, with the three task-based paths first and a card per topic saying in one line whether it is the page you want. - Every topic page links to the previous and next one, so you are never left at a dead end.
- Links you already shared still work. An old
/docs#api-keyslink forwards to the new page automatically, driven by the same topic list as the pages themselves, so a renamed topic cannot fall out of it.
v0.2.185
Added
- The docs now start by asking what you are trying to do. They were twelve reference sections on one page, which tells you what exists but not which section answers your question. There are now three ordered paths at the top, one for putting an API in front of data you already have, one for starting from nothing, and one for integrating against an API someone else built here. Each step links straight to the place that does it.
- Every link in those paths is checked by a test, so a documented step cannot quietly point nowhere.
Changed
- The reference sections are unchanged and every existing anchor link still works. They are now introduced as material to look up rather than to read front to back.
v0.2.184
Added
- The BigQuery byte ceiling is now adjustable per resource. A query is capped at 10 GiB billed by default, which stops an unexpected bill; if a legitimate request is being rejected you can raise it on the resource, up to 1 TB. Editing a resource keeps whatever you set rather than resetting it.
v0.2.183
Added
- Connecting BigQuery no longer means knowing your own ids or typing your fields. In the resource builder, enter your project and service-account key, then list what those credentials can reach. Pick a table from your own datasets and the dataset, table and region are filled in for you, a sample of real rows is read, and the fields are detected from it. Review them, adjust anything, save.
- The detected schema says what it changed rather than changing it quietly: columns renamed to fit the field-name rules, columns the platform handles for you, and any columns left out because a resource can expose 50.
- The table list is filterable and capped, so a project with hundreds of tables is navigable rather than a wall.
v0.2.182
Added
- Cursor pagination for BigQuery and external Postgres resources, so a large table is fully reachable. Offset paging stops at 100,000 records, which at 100 per page is 1,000 pages: a table of millions could not be read to the end. A list response from these sources now carries
pagination.nextCursor, and passing it as?cursor=returns the next page with no ceiling. On BigQuery it is also cheaper, because an offset makes BigQuery read and discard every row it skips, so you were billed for data you never saw. - The
cursorparameter appears in your resource's OpenAPI and docs only for sources that support it. Passing one to a source that does not is rejected rather than quietly returning the first page again.
v0.2.181
Added
- Firestore resources can be sorted by your own fields. Results are ordered by the field you name, with the document id as a stable tiebreak so paging cannot drift.
Changed
- Sorting a Firestore resource by
createdAtorupdatedAtis now rejected instead of returning nothing. Firestore does not maintain those fields, and it leaves out any document that lacks the field being sorted by, so ordering by one of them would have returned zero records with a success status. The guide explains the same rule for your own optional fields: sorting by a field some documents do not have returns fewer rows than not sorting, which is Firestore behaviour rather than a filter.
v0.2.180
Changed
- REST-backed resources now say plainly that your query is forwarded, not applied. EndpointOS relays filters and paging to the upstream API and cannot know whether that API honours them: an upstream that paginates by cursor returns its first page forever, and an unrecognised filter name returns an unfiltered collection. That was previously listed as plain support, which was a promise made on the upstream's behalf. The setup guide, your customers' docs, and the published contract all now state it.
v0.2.179
Added
- Firestore resources support filtering and offset paging. Both were previously rejected: the endpoint the connector used applied no filter and understood no offset, so honouring them was impossible. It now issues a Firestore structured query, which means the database does the filtering. Equality, not-equal, one-of and the four comparisons all work, and paging past the first page works.
- Firestore results are explicitly ordered by document id, so two page requests cannot overlap or skip documents.
Changed
- Filter values sent to Firestore are converted to its typed representation, so a number filter matches a number field. Firestore treats
42and"42"as different values, which means a field's declared type has to match what Firestore stores or filters match nothing. The guide now says so, along with what to do about the composite index Firestore asks for on some filter combinations.
v0.2.178
Added
- The docs now have a "Connect your data" section listing every data source and its setup guide, in the sidebar and in docs search. The guides existed but were reachable only through a link on a source name inside a paragraph, which meant finding them required already knowing they were there. The list is generated from the sources themselves, so a new one cannot ship without appearing.
v0.2.177
Added
- Supabase resources support the full filter grammar.
ne,in,gt,gte,ltandlteare translated to PostgREST's own operators instead of being rejected. Two filters on the same column now both apply, so a range likeyear[gte]=2020&year[lte]=2024works. Your resource docs and OpenAPI update themselves to match.
v0.2.176
Fixed
- BigQuery list responses now report a real record count. The pagination total was the number of rows in the page, so a consumer could not tell a full page from the end of the data and could not page reliably. It is now a genuine count over the same filter as the page, cached for a minute so paging through a large result costs one count rather than one per page. Your resource's docs and OpenAPI already state which sources report an exact count, and BigQuery now says exact.
v0.2.175
Fixed
- The docs you hand your customers now describe what each resource actually supports. The filtering section promised comparison operators, text search and sorting for every resource regardless of where its data lives, and its example used three controls that a BigQuery-backed resource rejects. Each resource now gets its own table of query controls, generated from what its data source implements, and the example request is one that resource will accept. An unsupported control is stated as an error rather than left to look like a no-op.
v0.2.174
Added
- Every data source now has its own setup guide, at
/docs/connect/bigqueryand one per source, linked from the data-source picker when you create a resource. Each covers exactly what to grant (named roles, not "read access"), the commands to grant it, what the source can and cannot do, and what to check when something fails. - The guides cannot drift from the API. The filtering, sorting, search, write and record-count rows on each guide are generated from the same declaration the API enforces on every request, so a guide can no longer promise something your request would be rejected for.
v0.2.173
Added
- BigQuery queries now carry a spend ceiling. Every query is capped at 10 GiB billed, and BigQuery cancels a job that would exceed it rather than running it. A request that trips the ceiling gets a clear explanation instead of a mysterious failure. Previously a single popular endpoint over a large table could bill a full table scan on every call, and the first sign of it would have been your invoice.
- Reads only fetch the columns your resource exposes. BigQuery bills by bytes read and stores data by column, so a resource exposing six columns of sixty now stops paying for the other fifty-four on every request.
Fixed
- A field that names a column your BigQuery table does not have now says so, naming the field. It previously came back empty with no explanation.
v0.2.172
Fixed
- BigQuery datasets outside the US now work. BigQuery assumes a query belongs to the US multi-region unless it is told otherwise, so a dataset in Zurich, the EU, or any other region failed every read with "not found in location US". The region is now read from your dataset automatically, both when the resource is saved and, for resources created earlier, on the next request. You are never asked what region you are in, and nothing needs re-entering.
v0.2.171
Fixed
- BigQuery resources now work with a hyphenated Google Cloud project id. Most project ids contain a hyphen, and the connector rejected them before sending a request, so every read against such a project failed. Its own setup hint suggested exactly the format it refused.
- A BigQuery query that does not finish in time is now reported as a timeout, not as an empty table. BigQuery answers with a success status and no rows when a job outruns its deadline, and the connector took that at face value, so a slow query over a large table looked exactly like a table with nothing in it. It now returns a clear error telling you to narrow the query, add a partition filter, or expose a view shaped for serving.
- BigQuery setup instructions named only half the permission needed. Running a query is a job, so the service account needs
roles/bigquery.jobUseron the project as well asroles/bigquery.dataVieweron the dataset. Following the old instructions exactly produced a permission error the instructions did not explain.
v0.2.170
2026-07-28Changed
- A filter your data source cannot apply is now an error instead of a silent success. The API documents seven filter operators (
year[gte]=2020,brand[in]=a,b, and so on), but only EndpointOS-managed storage applied them. Supabase, external Postgres, REST, BigQuery, and Firestore all discarded the operator and returned HTTP 200 with unfiltered data, which a consumer has no way to detect. Those requests now return 400UNSUPPORTED_QUERY, naming the field, the operator, and the operators the source does support. The same applies tosort,offset, and theqsearch parameter. - Generated OpenAPI now documents only the query parameters your resource's data source can honour, so a generated client never sends a parameter the contract promised and the API rejects. Per-project docs describe the new error code.
- Firestore resources reject filter parameters rather than returning the whole collection, and reject
offsetrather than silently returning the first page again. Both are planned for a later release. - A mistyped filter operator or sort field is now an error.
?year[gtequal]=2020and?sort=-creatd_atpreviously returned every record with a 200, indistinguishable from a query that worked.
Added
- External Postgres resources support the full filter grammar.
ne,in,gt,gte,lt, andlteare translated into parameterised SQL instead of being dropped. The row count and the page now share one predicate, so the pagination total describes the filtered set. - Every data source declares what it can do (filter fields and operators, sort, search, writes, pagination, whether it can produce an exact total), and the API enforces that declaration on every request.
Fixed
- Sorting a managed resource by one of its own fields was ignored. A request carrying only
?sort=voteswas served in newest-first order with a 200 and no indication the sort had not been applied. Sorting bycreatedAtorupdatedAtis now handled by the database directly, and sorting by a field of your own is applied to the payload. - Descending sort was ignored on external Postgres, Supabase, and BigQuery.
?sort=-idreturned ascending results. Those sources now apply the direction, and reject a sort by any other field instead of answering inidorder as though it had worked. - Supabase pages are now explicitly ordered. Without an explicit order, two page requests could return overlapping or skipped rows, because the database gives no ordering guarantee across separate queries.
- Filtering an ingested stream by its identity field works again. The filterable field was registered under the wrong name, so filtering by
device_idwas rejected as unknown while the readings came back unfiltered.
v0.2.169
Changed
- The Ingest page now leads with the module, not the vocabulary. It previously took its heading from whichever vocabulary was selected, so a project on the default read as "IoT telemetry" with nothing to indicate that was a choice. The heading is now Ingest, and the active vocabulary sits below it as a control you can change without leaving the page.
- New projects start on a domain-neutral vocabulary. Generic JSON keeps every field as sent and recognises only the identifier, type, and time. Existing projects are unaffected: they keep whatever vocabulary they already had. Defaulting to a specific domain made a general-purpose engine look like a single-purpose one.
- Ingesting data and publishing an API are now visibly separate steps. The page opens with the three steps (ingest, review the detected schema, publish), each showing live state, and an unpublished stream says plainly that it is stored but not exposed. A schema appearing on its own used to read as though provisioning had already happened.
Added
- Vocabulary suggestions now appear on the Ingest page, next to the switcher, rather than only in settings. If your data looks like a different domain, you see it where the data is.
- See what a vocabulary would do before you switch to it. Each option shows the exact fields it would return, produced by running one of your own recent payloads through the real normaliser, and warns when it would find no identifier in your data.
- Detection now shows its work. "Why this vocabulary?" ranks every vocabulary against your recent payloads with a fit score and the reasons behind it. It stays advisory: nothing is ever switched for you.
- Every detected field explains itself. Open a field in the schema to see what the system understood it to mean and exactly where it lands in the API response: mapped to a shared name, passed through unchanged, folded into the envelope, or read for context and not returned.
- The quickstart now tells you which API key to export, or offers to create one when the project has none, and the fastest measured ingest-to-API time is shown alongside the steps it timed.
Fixed
- Pricing suggestions on the Ingest page used IoT's rules regardless of the project's vocabulary, so a commerce stream was reasoned about as if it carried energy readings.
- The sandbox endpoint handed every project an IoT sample payload. It now returns one the project's own ingest endpoint would accept, and reports the active vocabulary.
v0.2.168
Fixed
- The ingest health endpoint now reports itself as the ingest module rather than the IoT module, and counts registered streams rather than device types. The module was renamed but this response was missed.
v0.2.167
Added
- The ingestion engine now works for any kind of data, not just device telemetry. Four vocabularies ship alongside IoT: commerce (orders, prices, quantities), events (user and product activity), metrics (revenue, rates, latency), and entities (users, accounts, products). Pick one per project under Settings. The vocabulary decides which field identifies a record, what the fields mean, what counts as an implausible value, and how the generated docs read.
- Records that are not time-series, such as a user profile, no longer have a timestamp invented for them. Previously an ingest time was substituted and then flagged as a clock problem on data that had no clock.
- The dashboard now speaks your domain's language. An IoT project says devices and readings; a commerce project says orders and transactions. Headings, table columns, empty states, and the quickstart example all come from the vocabulary, so the page reads correctly whichever kind of data you send.
- Vocabulary suggestions. If your recent payloads look like a different domain than the one selected, project settings says so and explains why. It never switches for you: a wrong automatic guess would reshape your schema and published contract silently.
- The simulator now only appears on projects using the IoT vocabulary, since it only generates device telemetry.
- The identifying field can arrive under any of its accepted names. A project ingesting entities can send
id,userId, oraccountIdand all are accepted.
Fixed
- Pricing suggestions now use the project's own vocabulary. They previously applied IoT's rules to every kind of data, so a premium for financial or transaction data never applied.
Changed
- Generated explanations no longer describe every dataset as coming from devices.
v0.2.166
Fixed
- The generated API contract now lists every field the API actually returns. A payload carrying a second identifier (a gateway id alongside a device id, say) had that field served in responses but missing from its own resource definition, so it was absent from OpenAPI and the SDK.
- Generated documentation now shows passthrough fields in its example response. Fields the engine did not recognise were being served but left out of the example, so the docs described a narrower response than the API returns.
v0.2.165
Added
- Modules are now opt-in per project. The ingestion capability (formerly the IoT tab) only appears on projects that turn it on, under Settings. A project serving a product catalogue no longer carries an ingestion tab it never asked for, and its API surface returns 404 until enabled. Projects already holding ingested data keep it switched on, so nothing disappears.
- Simulated data is now labelled and removable. Readings written by the built-in simulator are marked as simulated, badged everywhere they appear, and clearable in one click. Previously demo data was written into the same tables as real telemetry with nothing to tell them apart, which meant a dashboard full of fabricated readings could read as production traffic.
- Detected streams can be published as API resources. A one-click action on the Ingest tab turns a detected stream into a read-only API resource, which brings it into the generated OpenAPI document, the public docs page, the TypeScript SDK, and per-resource analytics. Until you publish, a stream is ingested but not exposed, so detecting a schema never silently publishes your data.
Fixed
- Ingested data can now actually reach the generated OpenAPI, docs, and SDK. The data source type behind it was never registered, so the platform silently treated such resources as EndpointOS-managed and nothing could create one. The capability was documented but did not work.
Changed
- The IoT module is now the Ingest module. The engine that detects a schema, normalises records, spots drift, records lineage, and generates docs was never specific to devices. It now works from a swappable vocabulary, with IoT device telemetry as the first one. Nothing about the IoT behaviour changes: the same fields are detected, the same alerts fire.
- API paths move from
/api/iot/...to/api/ingest/...for the same reason. If you saved any IoT endpoint URLs, update the prefix. Everything else about the requests and responses is unchanged.
Fixed
- IoT auto-suggestions now actually appear. The engine could already tell you that a field named
tempmost likely means temperature, but nothing ever surfaced it, so the advice was generated and discarded. Suggestions now show up in the alerts list and on the IoT dashboard whenever a device type's schema is created or changes.
Changed
- The IoT ingest endpoint now returns snake_case field names, matching every other IoT endpoint.
deviceIdbecomesdevice_id,deviceTypebecomesdevice_type, andschemaVersionbecomesschema_version, in both thereadingsanderrorsarrays. If you were reading those fields from an ingest response, update the names. Everything else about the response is unchanged. - Generated IoT documentation now lists the query parameters each endpoint accepts. The filters and pagination options were always supported but were missing from the docs.
v0.2.163
Added
- IoT module. Post device telemetry to a project and the API builds itself. The schema is detected from the payload (types plus semantic meaning, so a field called
valuecarrying acelsiusunit is understood as temperature), readings are normalised into a stable shape across every vendor spelling, and the endpoints, documentation, and code snippets are generated from that schema. There is no provisioning step to wait for. New tab under any project, and a device simulator so you can see it working without hardware. - Self-healing intelligence. The module watches incoming data and raises alerts for schema drift (new fields, fields that stopped reporting, type changes) and for anomalies (out-of-range temperatures, impossible percentages, missing or future-dated timestamps). Alerts deduplicate while open, so a stuck sensor produces one alert rather than one per reading.
- Flight recorder. Every reading keeps its full lineage: the raw payload as received, the processed output, the schema version that processed it, each transformation applied, and any issues found. You can replay a historical reading through the current pipeline and diff it, which is how you see what a schema change did to data you already collected.
- Explain my data. Ask what a device type's data actually represents and get a plain-English summary, per-field meanings with units, and plausible use cases. It says when a field was not recognised rather than inventing a meaning for it.
- Monetisation suggestions. Each device type gets a recommended pricing model and a price per 1,000 calls, with the reasoning shown. Suggestions only. Metering continues to run through the existing usage pipeline.
- Time to first API. The module measures how long it took to go from the first reading to a working API, and from there to the first call against it. Visible per device type in the dashboard.
- New public API surface under
/api/iot, plus/api/iot/healthfor uptime monitoring. IoT data also flows through the existing OpenAPI, docs, and TypeScript SDK generators when exposed as a resource.
Operational
- The admin health page now reports whether the database has actually run every migration in the current deploy, and a daily check raises an alert when it has not. A migration that is written but never applied leaves the code querying columns that do not exist, which is how the per-resource analytics page broke earlier without anyone noticing.
v0.2.161
Added
- Per-project docs branding now takes a logo URL. Set an https image URL in project settings and it replaces the initials mark at the top of your public docs page, alongside the display name and accent color you can already set.
Fixed
- Per-resource analytics now load. The page queried a column that existed in the schema but had never been applied to the production database, so it failed for every project. Applying that migration also brings in the request-log indexes it shipped with, so analytics over a date window are considerably faster.
v0.2.160
2026-07-27Fixed
- Workspace members and pending invitations now fall back to a card layout on narrow screens, so the row actions stay reachable instead of being pushed off the side of the table.
- Raised several dashboard controls to a full-size tap target on mobile, including the account settings audit-log filter bar and the recovery button on "not found" pages.
Security
- Updated the outbound HTTP library to its current release.
v0.2.159
Security
- Workspace invitation links no longer travel in the page URL. A live invite grants workspace membership, and in the query string it also landed in browser history, in referrer headers, and in access logs. The link now passes through a one-time server-side handoff and is still shown once with its copy button, so nothing changes in how you use it.
- Updated the outbound HTTP, database, auth, and payment libraries to current releases.
Fixed
- Generated OpenAPI documents and TypeScript SDKs now list resource fields in a stable order. Previously two fields created in the same millisecond could come back in a different order on each fetch, so an unchanged project could emit a slightly different schema each time you downloaded it.
- The webhooks and consumers pages now offer a button that jumps to the create form when the list is empty, instead of pointing at a form that sits below rather than beside the empty state on a narrow screen.
- Fixed the per-consumer and per-key analytics pages on mobile: the time-window picker is now a full-size tap target, and the top-endpoints list no longer leaves an empty column where the usage bar is hidden.
v0.2.158
2026-07-26Security
- Consumer portal secrets (a freshly rotated API key, a newly minted share-link token) are no longer handed to the next page render through a JavaScript-readable cookie. They now travel in an HttpOnly cookie that the server reads once and clears, so injected scripts cannot read them. The value is still shown and copyable exactly once, so nothing changes for you.
- The unauthenticated custom-domain resolver is now rate limited per IP, so it cannot be used to enumerate custom domains.
- Patched the image-processing and framework dependencies behind several published advisories.
Fixed
- CSV import no longer silently rewrites number values it cannot read cleanly. Hex, octal, binary,
Infinity, and values too large to represent exactly used to be coerced into something you never typed. They are now reported as per-row errors with a suggestion, so a row either imports the value you gave it or tells you why it did not. - When a workspace goes over its plan's project cap, the projects chosen for pausing are now picked in a stable order. Projects created in the same millisecond could previously pause arbitrarily between runs.
- Dashboard lists and generated docs now sort in a stable order when two rows share a timestamp, so paging no longer skips or repeats entries.
- Request-log status badges no longer show 3xx responses as success or 4xx as failure.
security.txt, the changelog feed, and alert links now use the origin the deployment is actually configured with, instead of always pointing at production.- Generated docs now mark server-managed fields on the schema table.
- Fixed several dashboard empty and not-found states that dead-ended on stale links, plus mobile tap-target sizes and form label associations.
v0.2.157
Security
- Public API OpenAPI and SDK downloads now cache against the latest published snapshot, not just the version string, so same-version republish updates cannot be hidden behind stale client caches.
- Versioned OpenAPI and SDK download routes now use the same rate-limit gate as the current-version routes.
- Connector egress hardening now blocks additional IPv6 private, site-local, and multicast ranges, caps buffered upstream response bodies, partitions Google service-account token cache entries by token URI and private key, and redacts full Authorization header values in logs.
- Account and audit-log exports now require the MFA gate when a workspace requires MFA.
- Stripe webhook retries are no longer permanently deduplicated when a handler fails before completing.
- The Stripe bootstrap helper no longer prints generated webhook secrets to the terminal.
- Correction (added 2026-07-08): this release's managed-connector ad-hoc-filter 413 response (over the resource's filter scan ceiling) changed its
error.codefromVALIDATION_ERRORto a distinctQUERY_TOO_BROAD, with an updated message. This should have been called out here at the time; integrators branching oncode === "VALIDATION_ERROR"for that specific 413 case should switch toQUERY_TOO_BROAD.
Fixed
- Public API body size checks now measure UTF-8 bytes, not JavaScript string length, so multibyte JSON cannot bypass the request cap.
- API contract publishing now rejects version labels that cannot be routed as
/vN. - Empty
NEXT_PUBLIC_APP_URLvalues now fall back to the local default instead of breaking metadata generation during builds.
v0.2.156
Security
- Hardened two redirect endpoints (the docs-page "request access" return and the workspace switcher) so a crafted link can no longer bounce you to an external site.
Changed
- Public API list endpoints now return records in a fully stable order across pages. Previously, when two records shared the same creation timestamp, paging through a large collection could occasionally skip or repeat a record. Pagination is now deterministic.
- The generated OpenAPI spec now documents the 400 and 403 responses these endpoints can return, and the rate-limit error detail clarifies the per-minute window for API-key and IP limits.
Fixed
- Per-key and per-consumer "Recent activity" tables now color HTTP status codes consistently with the rest of the dashboard: 3xx redirects read as neutral and 4xx client errors as warnings, instead of being mislabeled as success or hard errors.
v0.2.155
Security
- Closed an internal analytics action that could be invoked without authentication.
- Public API now rate-limits project resolution before any database work, keeps archived and paused projects masked from unauthenticated callers, and honors a resource's current read-only setting even when an older published contract still allowed writes.
- Public docs endpoints (OpenAPI, TypeScript SDK, changelog) are now rate-limited and no longer serve archived or paused projects.
- Unique fields on managed records are enforced atomically, so two simultaneous writes can no longer both create the same "unique" value.
- API key permission changes and consumer monthly-limit changes now take effect immediately instead of lagging by up to a minute.
- Outbound webhook delivery rejects additional private and reserved IP ranges.
Fixed
- A rejected public API request (for example a validation error) no longer consumes one of your monthly request-quota slots.
v0.2.154
Fixed
- Dashboard "Recent traffic" and "Errors" cards now show the endpoint path. The old rigid column layout squeezed the path down to a couple of characters (
/a...) while reserving space for less useful details. The request line now gives the path the room it needs (with the full path on hover), and time and duration move to a quieter second line.
v0.2.153
Fixed
- Dashboard traffic and errors consoles: the status code badge (for example
401) no longer balloons into a large dark block when a request row also shows an error message. The badge now stays a tight pill aligned with the request line.
v0.2.152
Added
- Consumer plans: define named tiers for your API consumers, each with a monthly request limit and a display price, and place consumers on them. Prospective consumers can pick a tier when they request access from your docs page, and a consumer's monthly limit follows their tier (editing a tier re-applies its limit to everyone on it). Charging consumers is not part of this release: prices are shown, not billed.
v0.2.151
Security
- Hardened link rendering on the changelog: links now only render for
http(s)and site-relative URLs, so a craftedjavascript:ordata:URL cannot become a clickable link.
v0.2.150
Fixed
- When a CSV import would exceed your project's storage or record limit, you now get the clear "limit reached, upgrade or delete records" message instead of a raw database error. The import was already rejected safely; this just makes the reason readable.
v0.2.149
Fixed
- CSV import no longer drops quote characters that appear in the middle of a value. A value like
5"x3"(an inch measurement) was silently stored as5x3, and a stray quote could merge adjacent cells or rows. Quotes are now treated as special only when they open a quoted field, matching the CSV standard. Properly quoted values (commas, escaped quotes, and newlines inside quotes) are unaffected.
v0.2.148
Security
- Consumer-portal key rotation is now rate limited per consumer. The portal is authenticated only by its share link, and each rotation mints a new key and fires a webhook, so a capped rate prevents a leaked link from being looped to mint excess keys or fan out webhook traffic.
v0.2.147
More resilient webhook retries.
Fixed
- Webhook delivery now uses its full retry schedule. A counting bug skipped the fast 60-second first retry (so a transient blip waited 5 minutes instead) and gave up after 7 delivery attempts instead of the intended 8. Failing endpoints now get the quick first retry and the complete ~25-hour retry budget.
v0.2.146
Fixed
- The OpenAPI now documents the
413response on create and update operations (returned when a request body is too large or the project storage cap is reached), so consumers generating clients from the spec handle it.
v0.2.145
Hardened client-IP handling.
Security
- The client IP resolved from forwarded headers is now length-capped and format-checked before it is used as a rate-limit key or stored on a request log. This protects self-hosted and custom-proxy deployments from an oversized or malformed forwarded header. Vercel-hosted deployments were already protected by the platform-attested header.
v0.2.144
More complete API contract for rate limits and outages.
Fixed
- The OpenAPI and TypeScript SDK now document all four rate-limit scopes the API can return in
details.scope(project,consumer,api_key,ip). The per-key and per-IP throttles were previously missing, so a client branching on the scope could not handle them. - The OpenAPI now documents the
503you get when a project is archived or paused, on every endpoint, so integrators can retry instead of treating it as an unknown error. - The 429 rate-limit description no longer implies every limit is monthly (the per-key and per-IP throttles reset within the minute).
v0.2.143
Honest error codes in the API contract.
Fixed
- The generated OpenAPI and the TypeScript SDK now report the rate-limit error code as the value the API actually sends (
RATE_LIMITED). They previously documented a lowercaserate_limitedthat the API never returned, so a consumer branching on the documented code would not match. - The OpenAPI
Errorschema now lists the full set of stable error codes, so you can see exactly which codes to handle.
v0.2.142
Insights polish.
Fixed
- The Insights status line on the project overview now keeps its headline at full contrast (it previously used a tone color that could fall below the accessibility threshold on light backgrounds). The colored icon and border still signal status at a glance.
- The "View" action on each Insights finding is now a comfortable tap target on mobile.
v0.2.141
Insights gets its own tab.
- Dedicated Insights tab on every project. Anomaly detection now has a proper home in the project nav (next to Logs), with the full list of what shifted in the last 24 hours.
- At-a-glance status on the overview. The project overview shows a compact Insights line ("All clear" or "2 signals need attention") that links straight into the tab, so you see whether to look without digging.
v0.2.140
Insights: your projects now flag what changed, on their own.
- Automatic anomaly detection on every project overview. A new Insights panel reads the last 24 hours of API traffic and tells you, in plain English, what shifted: error-rate surges, traffic spikes and drops, p95 latency regressions, named consumers on track to blow their monthly limit, and active consumers that suddenly went quiet.
- Built to be trusted, not noisy. Every figure is computed from your real request logs (no invented numbers), low-traffic projects are protected by a minimum-volume floor so small swings don't cry wolf, and each finding deep-links straight to the logs or consumer it's about.
v0.2.139
Snappier dashboard, sturdier webhook IDs.
- Dashboard pages respond faster. Opening a dashboard page used to re-check your session with the auth service up to five times before loading your data; it now happens once per request. Navigation feels noticeably quicker, and your API consumers get the same benefit: public API calls no longer carry a session check they never used.
- Webhook event IDs are now cryptographically random. If your endpoint dedupes deliveries by event id (the standard idempotency pattern), two events published in the same second can no longer collide. IDs got a little longer; everything else about the payload is unchanged.
v0.2.138
Bring your own data source. Plus a security hardening pass.
- External connectors are live. Point a resource at an external Postgres, REST API, BigQuery, or Firestore source and serve it through your EndpointOS API, generated docs, and SDK, with the same auth, validation, request logs, and usage limits as your managed resources.
- Tighter security, nothing to do on your end. A full security audit closed a gap where the second factor could be skipped on the way into the dashboard and admin, and hardened how the platform makes outbound requests for connectors and webhooks.
v0.2.137
Fewer reasons to retype, fewer dead ends.
Three small upgrades that smooth the rough edges of everyday use.
- Forms remember what you typed. When sign-in, sign-up, project creation, workspace creation, or the contact form bounce back with a validation error, every field you filled in stays filled in. Passwords are the only exception, by design.
- Wrong URLs land in the right context. A typo in a dashboard URL drops you back at the dashboard with shortcuts to projects and settings, instead of the marketing 404. Same for admin pages, customer-facing public docs, and the sign-in flow.
- Render errors keep their shell. When a page errors out, you stay inside the dashboard, admin, or sign-in flow you were already in, with retry and "go back to known surface" actions. No more bouncing to the generic marketing error page mid-task.
v0.2.124
A ⌘K command palette across the whole dashboard.
Hit ⌘K (or click the new Search button in the top nav) from anywhere in the dashboard and jump directly to any page, any project, any resource, or any consumer in your workspace. Recently-used items pin to the top across sessions. Sectioned results, inline keyboard hints, fuzzy search, all keyboard-first.
v0.2.123
Revoking an API key takes effect immediately again.
Previously, after a Revoke click, the public-API cache could keep authenticating the old key for up to 60 seconds. Revocation now writes a kill-list entry that's checked on every cached resolution, so the next request the integrator makes after you click Revoke is the first one rejected. Same instant cut-off when a rotation completes: the old key transitioning to revoked is propagated the same way.
v0.2.120
The list endpoint now works on resources with thousands of rows.
A plain GET /resource?limit=20 used to load every row in the resource into memory and slice in JS. It now asks Postgres for just the page you want. Resources with thousands of rows go from "times out or returns megabytes of JSON" to "responds in milliseconds."
v0.2.119
Security + correctness sweep.
A focused round-up of edge cases caught by a multi-agent bug hunt:
- Public API responses set
Cache-Control: private, no-store. A misconfigured corporate proxy or CDN can't accidentally cache one tenant's response and serve it to another. - Consumer portal pages set
Referrer-Policy: no-referreras a real HTTP header. The signed token in the URL can't leak viaRefererwhen a visitor clicks an outbound link, even on browsers that ignore<meta name="referrer">. - Email-link sign-in refuses to confirm unverified addresses. Closes a path where an OAuth provider that signed in users without confirming could overwrite an existing EndpointOS account that already owns the same email.
- The
next=redirect target on auth links is strictly validated. No protocol-relative URLs, no scheme prefixes (javascript:,data:), no parent-directory escapes, no whitespace. A phishing link can't bounce you off-origin after sign-in. - Stripe overage metering carries an idempotency identifier on every meter event. A cron retry after a partial failure (Stripe call succeeded, database write failed) can't double-bill a workspace.
v0.2.118
Download a fully typed TypeScript SDK straight from your docs.
Every published API now exposes a one-click TypeScript SDK download next to its OpenAPI link, on both the public docs page and the project dashboard. It's the same fully-typed client the CLI emits, generated from your live contract, so integrators can grab a working file in a single step. Versioned download lives at /sdk/typescript under each contract version too.
v0.2.115
Rate limits now hold across the whole fleet.
Per-key and per-IP rate limits used to live in each Vercel function instance's memory, which let a determined client burst past the limit by hitting different cold starts. They now use a shared store (Upstash Redis) so the limit is exact across the fleet. Flips on the moment Upstash creds are present in the environment; without them, behaviour stays exactly as before.
v0.2.104
Your API key count is honest now, and rotation stops mangling key names.
Two related fixes. The "API keys" number on /plans (and everywhere else) used to include revoked keys and double-count the brief window where an old key and its replacement coexist during rotation, so the count drifted higher than what you actually had. Now a rotation pair counts as one logical key, revoked keys are excluded, and the number on /plans matches reality. Also: rotating a key used to rename it "Production (rotated)", and rotating that produced "Production (rotated) (rotated)" and so on. New keys now inherit the original name; the rotation status shows as a badge on the old key during the grace window and goes away after.
v0.2.103
Workspace-wide storage cap, plus your storage usage on /plans.
Plans with unlimited projects (Team, Business) now also have a workspace-wide total cap on managed records and bytes, so big-tier customers can't accidentally exceed what their plan is sized for. The /plans page shows your workspace storage usage at a glance. Plus a fix: editing a record to make it much larger now respects the storage cap, the same way creating a new record already did.
v0.2.102
Managed-data storage is now capped per plan, with clear warnings before you hit the limit.
Every project gets a managed-record cap (rows and bytes), scaled to your plan, with 80% and 100% banners on the project page so you see it coming. Going over returns a clear error instead of a surprise bill. The cap is enforced in the database, so no API path can sneak past it. Soft-deleted records are hard-purged 30 days later, so delete actually frees up your quota.
v0.2.101
Lost the device with your authenticator? We can get you back in.
If you lose access to your two-factor codes, support can now clear the old authenticator from your account so you can set up a fresh one and sign back in. No more permanent lockouts.
v0.2.96
Connectors you can actually create, and a safety net on destructive actions.
Fixed a bug where choosing an external data source (Supabase, Postgres, REST, BigQuery, or Firestore) for a resource was silently ignored, so you'd end up with a plain managed resource instead. It now respects your pick. We also added a confirmation step before revoking an API key, completing a rotation, or deleting a resource or webhook, so one stray click can't take down a live credential.
v0.2.84
Connector errors no longer hold onto secrets.
When an external data source (Postgres, Supabase, a REST API, BigQuery, or Firestore) throws an error, the diagnostic we save and show you is now scrubbed of connection strings, tokens, and keys first. Those details never reached API callers, but now they don't sit in your dashboard either.
v0.2.79
Bring your own domain for your docs.
Serve a project's docs on a hostname you own (like docs.acme.com) instead of the EndpointOS URL. Add the domain in project settings, point one DNS record at us, hit Verify, and your docs go live on it with TLS handled for you. Remove it whenever you want. One domain maps to one project, so there's no mixups.
v0.2.78
Usage-based overage billing for workspaces that opt out of caps.
Turn spend caps off and you trade the hard 429 for pay-as-you-go: requests above your monthly plan cap are now allowed and billed at $10 per 100,000 requests. With spend caps on (the default), you still hard-stop at the cap and are never charged for traffic you didn't approve.
How it works: a daily job reports your over-cap usage to Stripe (attaching the metered overage line to your subscription only if you actually exceed the cap), and it shows up on your next invoice. The /plans page shows your running overage estimate while you're over.
v0.2.77
Developer portal: people can request API access from your docs.
Your public docs page now has a "Request API access" section. A developer fills in their name, email, and what they want to build; you get the request in your project's Consumers tab. Approve it and EndpointOS creates a named consumer for them, then drops you on their page to issue a key and a private share link to send back. Deny it and it's gone. Honeypot + rate limiting keep the spam out.
This turns "email me for an API key" into a real self-serve onboarding flow, built on the consumer + share-link plumbing already there.
v0.2.76
Signup asks you to confirm you're 16+ and agree to the terms.
A required checkbox on the signup form, the way every account signup should work. We don't ask for a birthdate (we collect as little as possible); it's a simple attestation plus agreement to the Terms and Privacy Policy. The privacy policy now also spells out exactly what the contact form does with your details (replies only, never marketing).
v0.2.74
Request logs now actually expire on schedule.
The plan ladder has always listed a log-retention window (7 days on Free, up to a year on higher tiers) and the privacy policy promised old logs are deleted automatically. That deletion is now real: a daily job sweeps request logs older than each workspace's retention window. Honours admin plan overrides, so a workspace granted longer retention keeps its logs that long. Nothing else (usage counters, audit history) is touched.
v0.2.73
Download your data, or delete your account. Both self-serve now.
Two privacy rights that used to require emailing support are now buttons in Settings, under a new "Your data" section:
- Export my data. Downloads a JSON file with your profile, workspaces, projects, resources, API key metadata, recent request logs, and audit history. No secrets, key hashes, or connector credentials are included. (GDPR Articles 15 + 20.)
- Delete account. Type your email to confirm, and we permanently erase your profile, sign-in, and any workspace you solely own (with its projects, resources, keys, records, logs). Workspaces with other members are left untouched (you'll be asked to transfer ownership first). Any active subscription is cancelled. (GDPR Article 17.)
v0.2.72
Cancel your subscription without leaving the app.
Until now the only way off a paid plan was to open the Stripe customer portal and cancel there. There's now a "Cancel subscription" card right on /plans: type CANCEL to confirm, and your plan stays active until the end of the current billing cycle, then drops to Free. No detour through Stripe's hosted pages.
Changed your mind before the cycle ends? A "Resume subscription" button appears while a cancellation is scheduled, so it's never a one-way door. When the cancellation does land, anything over the Free project cap pauses (never deletes), same as any downgrade.
v0.2.71
Connector health: see when an external data source is failing.
Resources backed by an external connector (Supabase, Postgres, BigQuery, Firestore, REST) now carry a health status: Healthy, Degraded, or Failing. It updates two ways:
- Automatically, from real traffic. When a public API request to a connector fails (upstream down, credentials rotated, table dropped), the failure is recorded on the resource. A few consecutive failures move it from Degraded to Failing. The next success clears it. Zero extra calls to your data source.
- On demand. A "Check now" button on the resource page runs a live ping and updates the status immediately.
v0.2.63
Internal write APIs are now rate-limited too.
The public API hot path, CSV exports, billing actions, and admin writes have always been rate-limited. The internal write APIs the dashboard calls behind your session (create project, create resource, create/delete API key, kick off a CSV import) were not. Filled the gap: 60 requests/minute per IP for normal writes, 10/minute for CSV imports (those are expensive). Legitimate use never sees a 429; runaway loops do.
v0.2.62
Second bug-hunt round on the billing surface.
After shipping the security + perf passes, we re-audited the new code. Five more edge cases worth fixing:
- Stripe webhook dedup is now atomic at the database layer. The previous check-then-write pattern had a race where two concurrent deliveries of the same event could both pass the check and both run the handler. Now: a partial unique index on the dedup row + an insert-or-409 pattern. Race-proof.
- Billing-action rate limiter no longer silently disables itself when the client IP can't be read from request headers (e.g., some misconfigured proxies). Falls through to a shared bucket so abuse traffic still gets throttled.
- Member-cap rejections in the plan-change commit path now show the actual reason ("Pro allows 3 seats; this workspace has 5. Remove 2 members first.") instead of the generic "couldn't complete with our payment processor" message that other Stripe failures get.
- If creating a Stripe subscription schedule succeeds but the follow-up phase update fails, we now release the half-built schedule so the subscription is left in a clean state instead of stranded mid-transition.
v0.2.60
Security hardening on the billing surface.
Self-audit on the v0.2.49 to v0.2.58 billing code surfaced a few things worth tightening before they bit anyone:
- Stripe webhooks now dedup by event id. Stripe retries any webhook on a non-2xx response (network blip, transient error). Without dedup we'd reapply plan changes, double-fire audit rows, and re-trigger pause logic on every retry. First receipt wins, subsequent receipts of the same event short-circuit.
- Billing actions are rate-limited at 20 / min / IP. A buggy client looping on Subscribe shouldn't be able to spam Stripe with hundreds of checkout sessions. Legitimate use never hits the limit; abuse gets a clear 429.
- Generic error messages on payment failures. We were surfacing raw Stripe error text directly to the URL bar, which included internal API details. Errors now log to the server for the operator to diagnose; users see a generic "couldn't complete with our payment processor" message.
v0.2.59
Bug-hunt sweep on the new billing flow.
Self-audit the billing code we shipped in v0.2.49 to v0.2.58 caught a handful of edge cases worth fixing before someone hits them in production:
- Reactivating a paused project could let two concurrent clicks both succeed and put your workspace over the project cap. Now wrapped in a serializable transaction with an update-with-predicate so only one wins.
- Downgrade scheduling now re-fetches the live subscription right before building the schedule, so a price changed via the customer portal between your click and our API call doesn't silently lock the wrong price onto the schedule.
- Member cap check is now re-verified inside the actual commit, not just on the button press, so a teammate added between your Confirm click and the Stripe call can't slip past the gate.
- Settings page redirects with a clear message if your active workspace has been deleted instead of rendering "you're on Free" for a workspace that no longer exists.
- Stripe webhook now validates that period timestamps are real finite numbers before turning them into Dates. Belt-and-braces against malformed payloads silently corrupting your billing cycle dates.
v0.2.58
Plan changes show you the exact charge before they happen.
Upgrading or downgrading on /plans now drops you on a confirmation page that spells out what's about to happen:
- Upgrades: "You'll be charged $3.42 today (prorated for the rest of this cycle), then $19.00/month starting July 12." Pulls the actual proration figure from Stripe before you click.
- Downgrades: "Takes effect on July 12 (end of your current cycle). Until then you keep Pro benefits. No refund or credit either way." The downgrade lands automatically on that date via a Stripe subscription schedule. Means you stop paying for what you paid for; you don't lose access to it.
v0.2.57
Downgrade safety. Your projects stop disappearing into the void.
When you downgrade to a plan that allows fewer projects than you have running, the platform now pauses the excess (oldest first) instead of leaving you over-cap in limbo. Paused projects:
- Return HTTP 503 with
PROJECT_PAUSEDfrom the public API so integrators see a clear "temporarily unavailable" signal. - Keep every record, resource, key, log, and webhook on disk. Nothing gets deleted.
- Show a banner in the dashboard with a one-click Reactivate button. Reactivate is gated by your new plan's cap, so pick which projects matter and pause / archive others to swap.
v0.2.50
Upgrades stop double-charging you.
This one's important. Until today, hitting "Upgrade to Pro" while already on Starter would spin up a brand new Stripe Checkout for the full $19, instead of just changing your existing subscription. Complete that and you'd end up with two subscriptions running side by side, getting billed for both. Now the buttons detect you have an active subscription and update it in place, charging just the prorated difference today.
v0.2.49
Billing is real now.
The Subscribe buttons aren't a bluff anymore. They fire a real Stripe checkout, the webhook flips your workspace plan in seconds, and the customer portal handles the rest. Two things we fixed today while we were watching the first test card go through:
- Settings now shows the plan you're paying for. It used to cheerfully claim "Free, early access" no matter what Stripe said. Fixed: it reads from your workspace like /plans does, and shows Manage subscription when there's a real customer behind you.
- Plan upgrades now charge the prorated difference today, instead of bundling it into next month's invoice and showing the confusing "starting next cycle" copy. Same outcome for downgrades, but the credit goes onto the next invoice (which is the standard SaaS move).
v0.2.48
2026-05-27Security tune-up.
Found a privilege-escalation bug in our own rotation code while reading it over coffee: rotating an API key that was scoped to specific resources was quietly upgrading it to an all-access key. That's now fixed, plus five smaller seams hardened in the same sweep (Firestore id extraction, scrubbed error messages, a Referer leak on portal links).
If you had any scoped keys rotated between v0.2.41 and this
release, double-check their current resourceScope. New rotations are
safe.
v0.2.47
BigQuery and Firestore. The connector count is now six.
- BigQuery (read-only): expose a filtered, paginated view of a warehouse table behind our auth + logs + quotas layer. Writes are refused with a clear message because BigQuery isn't an OLTP store and pretending otherwise would be irresponsible.
- Firestore (full CRUD): point a resource at a Firestore collection over the REST API. We translate Firestore's typed-value format to and from plain JSON at the boundary so your clients never see the wire shape.
v0.2.46
Your customers can now rotate their own API keys.
The old flow: integrator loses their key, emails you at 11 PM, you log
in and rotate it. The new flow: they open the portal link you sent
them, click "Rotate key (7-day grace)", and roll the new key into their
app while the old one is still working. The audit row and webhook
event both carry viaPortal: true so you can still see exactly what
happened and who initiated it.
v0.2.43
Consumer portal (share-link).
You add a customer as a named consumer in EndpointOS, click "Generate
share link", send the URL out-of-band. They land on /portal/c/{token}
with no account, no login, and see:
- Their consumer name and description
- The API key prefixes attached to their consumer
- Monthly usage vs their cap
- Your project's base URL and docs link
v0.2.42
External REST connector.
Point a resource at any HTTPS endpoint: a partner API, an internal microservice, a SaaS that doesn't quite fit your customers' workflow. EndpointOS becomes the stable, branded, observable front door; we proxy CRUD to whatever's upstream over plain HTTP. Optional auth header, SSRF guard on every call, the test-connection button you'd expect.
v0.2.41
Per-resource API key scoping.
Issue a key that can hit only /products. Issue another that can hit
/products and /orders but not /invoices. Useful for partner
integrations, internal microservices, and any time the answer to "what
does this key do?" is more nuanced than "everything in this project".
Existing keys keep their "all resources" scope; the new picker shows up on the create-key form for new ones.
v0.2.38
Project archive.
Some projects aren't gone, they're just resting. Archive a project to:
- Drop it out of your workspace's project count (so the cap stops biting you)
- Return
503 PROJECT_ARCHIVEDon every public API call (so old integrations fail loud, not silent) - Preserve every resource, record, key, log, and webhook, ready to unarchive
v0.2.35
External Postgres connector.
Bring your own Postgres: Neon, RDS, Supabase Postgres, a database
running on a Raspberry Pi in your closet (we won't judge). Connection
string encrypted at rest, parameterised queries only, SSRF guard
re-checks the host on every connect. Same shape as the Supabase
connector but routed straight through pg.
v0.2.34
Spend caps. So your bill doesn't surprise you.
On by default. Requests above your monthly limit return 429 until
the cycle resets. Toggle off on workspace settings if you'd rather eat
the overage. Banners on the dashboard at 80% and 100% so the number
doesn't jump-scare you next time you log in.
v0.2.31
Upgrade inline on /plans.
You no longer have to leave the dashboard to subscribe. Pick your tier, pick monthly or annual, hit Subscribe. Manage subscription button on the same page once you're a customer.
v0.2.20
Supabase connector.
Point a resource at a table in your own Supabase project. EndpointOS sits in front with auth, validation, logs, and request quotas; record CRUD is proxied to your Supabase over PostgREST. Service-role key encrypted at rest like every other connector secret.
v0.2.16
Retry now button on webhook deliveries.
Your endpoint went down for 10 minutes. It's back. You don't want to wait for the next scheduled retry slot. Click Retry now, the delivery fires immediately with a fresh attempt budget.
(The dull-but-important release before this one made the webhook
queue durable, with exponential backoff 60s → 5m → 30m → 2h → 6h →
12h → 24h over 8 attempts. So even if you forget the Retry button
exists, we will keep trying for you.)
v0.2.4
Workspaces, properly.
You can finally rename a workspace, delete it, or transfer ownership. The switcher shows your plan and links to settings, all workspaces, and the new-workspace flow. The projects page got a grid + search + sort treatment along the way.
v0.2.0
Team accounts.
Every project now lives inside a workspace. Invite collaborators by email, manage roles, share access without sharing your password. Solo users got a personal workspace automatically and noticed nothing.
Owners can invite, remove members, and change roles. Members can build: create resources, issue keys, view logs, manage consumers. Invitations are SHA-256 hashed, single-use, 7-day expiry, and the acceptance flow verifies the signed-in email matches the invited one (so a leaked link still can't be redeemed by a stranger).
v0.1.1
The kitchen-sink release. The one where this became a product.
- Admin console at
/admin. Cross-tenant search, contact-message inbox, platform audit log, live health dashboard. - Two-factor authentication (TOTP) for everyone, enforced on
/admin. Apple Passwords, 1Password, Bitwarden, Google Authenticator. All work. - Per-resource analytics at
/projects/:id/resources/:resourceId/analytics: traffic, status mix, top endpoints, recent activity. - Audit log filters and CSV export on the settings page.
- CSP nonces on
script-src(sounsafe-inlineis gone). - Privacy policy, Terms, Security overview, Contact form all shipped.
- Public
/changelogpage rendering this file. You're reading the output.
See the same notes as a tagged release on GitHub. Found a regression? Email support@baseframelabs.com.