Appearance
Changelog
All notable changes to SchemaStack are documented here.
See also: What is SchemaStack? · Getting Started · Key Concepts · API Reference · Roadmap
2026-08-29
Fixed
- Background work that failed left no trace beyond a log line. When the system finished changing your database but then failed to record that change in its own metadata, the message describing the work was discarded — leaving your database migrated, SchemaStack's picture of it stale, and nothing but a log line to say so. Those failures are now kept in a dead-letter queue where they can be inspected and replayed. Two related corrections came with it: only one worker now processes these messages at a time (a setting that had been configured for a long time under a name the message library silently ignored, so it had never actually taken effect), and a message that cannot be read at all is now preserved rather than dropped. Messages the system deliberately skips — ones it has no handler for — are still skipped, not treated as failures.
- A table with a two-part key always looked like it had drifted. The quick drift check compares a fingerprint of your metadata against one taken from your database. The database side counted each column of a foreign key; our side counted the key once, using only its first column. A two-column key therefore never matched, and the check reported drift on every run, permanently, with nothing to show for it. Single-column keys were unaffected and their fingerprints are unchanged, so only workspaces that were already stuck report anything different now.
- Composite foreign keys created inside SchemaStack reported their own columns as changes made behind your back. The second and later columns of such a key were invisible to drift detection, so it flagged them as foreign keys appearing in your database that SchemaStack did not know about. Imported keys were never affected.
- A column that gained or lost its primary key in your database kept the old answer. Re-syncing refreshed everything about that column except whether it was a key — while the flags derived from being a key were refreshed, so the two disagreed. Anything reading it (query building, how relationships are classified, drift) trusted the stale value.
- An AI agent restricted to certain views could act on all of them. MCP keys can be scoped to specific views. That restriction was only enforced for reading and writing data — every other operation checked that the target was in the same workspace, which a scoped key always passes. In practice a key limited to one view could delete a different one, drop its indexes, rewrite its saved filters, or rename it. It could also reset the whole workspace's schema. Every tool now checks the scope, and a key with no view scope is refused outright for workspace-wide operations like schema import and reset. If you have issued scoped keys, they are now genuinely limited to the views you chose.
- A foreign key renamed or restructured in your database is now reported. Drift detection could only tell you when a foreign key's cascade rules changed. Renaming a constraint, re-pointing it at a different table, or splitting a two-column key into two single-column keys all went unreported. All of them are now named in the drift report. Foreign keys created inside SchemaStack are picked up on the next sync.
- Dropping a foreign key no longer makes the column disappear. When a foreign key constraint was removed from your database, SchemaStack deleted the relationship — and left the underlying column hidden. The column and its data were still there, but the table rendered without them and without saying why. The column is now shown as an ordinary one.
- A table filter that matches nothing now says so. Import filters are SQL patterns, where
_matches any single character — sorepair_matches every seven-character name rather than tables starting withrepair_. That imported nothing and reported success. It now warns, and the API documentation explains the pattern rules.
2026-08-28
Fixed
- Accounts scheduled for deletion were never actually deleted. Confirming account deletion starts a seven-day grace period and sends you an email saying your account will be permanently deleted on a given date. The sweep that performs that deletion existed and worked — but nothing ever ran it. No schedule, no trigger, only the tests called it. Every confirmed account has therefore been sitting in the pending state past its promised date, with its data retained. The sweep now runs hourly, so a grace period that has elapsed is acted on within the hour. If you requested deletion and are still able to sign in, your account will be removed on the next run.
- Deleting a column left a fragment of it behind, and the grid was never told. When a column delete finished its database migration, the metadata row describing that column was supposed to be removed along with it. It never was — the removal failed instantly, the failure was caught and logged as success, and the rest of the work was committed anyway. Two consequences: an invisible leftover row per deleted column, and the "column deleted" event that tells every open browser to update was never sent, so the column lingered on screen until a refresh. Both are fixed. This was never a new problem — it dates back well beyond this week — and it stayed hidden because the test covering it had never actually executed.
- Reordering a column sometimes moved only that column. Dragging a column to a new position is supposed to shift the others out of its way. In some cases the shift silently did nothing and reported success, leaving two columns claiming the same position and an order that looked arbitrary afterwards. Fixed.
- Schema drift now notices foreign keys, in all four directions. Comparing your database against SchemaStack could only ever report a changed cascade rule on a foreign key. Adding one, removing one, or re-pointing one at a different table were all invisible — the check would tell you something had drifted without saying what. All four are now reported by name. Relatedly, relationships created inside SchemaStack were reported as newly-added foreign keys every time you checked for drift; drift now reads the relationship itself rather than one denormalised copy of it, so that false alarm is gone.
- "Reset schema" reported that it imported no columns. The summary after a reset always said zero columns imported, however many it actually re-imported. It now reports the real number. (The count it would have used was also wrong — it counted every recorded change, including views created and relationships added, not columns.)
- A duplicate column could never be cleaned up. If your metadata ended up with two entries for the same column — say
idandID— re-importing was supposed to resolve it. Instead the duplicate was quietly skipped and survived forever, and worse, each re-import handed it a fresh column in your view. Re-import now removes it, keeping the one that matches your database exactly, case included. - Changing a relationship's edit mode ignored some of its own defaults. Switching a relationship column to a different edit mode while also setting one of its behaviour toggles left the other toggles carrying the previous mode's values — so a column could end up in a state neither mode defines. Setting the mode now applies that mode's defaults, and anything you set explicitly still wins.
- A relationship can no longer be pointed at the wrong column. Naming the target column when creating a relationship was accepted and then silently discarded unless you named two or more, so the check that refuses to point a foreign key at another foreign key never ran for the ordinary single-column case. Named target columns are now honoured and validated. If you don't name one, SchemaStack still finds the primary key itself — which is what the relationship picker now asks for explicitly, instead of sending the column you picked to display.
Added
- Two new posts on the blog. The table that points at itself — modelling hierarchies where one table's foreign key points at its own primary key: categories with a parent category, employees with a manager, comments replying to comments. It covers building one from the picker, reading a parent and grandparent in the same grid, and what is still missing, including that a self many-to-many is not offered. Importing the same file twice — naming the columns that identify a row so a re-import skips or corrects instead of duplicating, why your database needs a unique index on those columns for it to mean anything, and the honest asymmetry that MySQL cannot validate the columns you named at all.
Fixed
- An AI agent can no longer change who is allowed to log in. Configuring a workspace's external identity provider — the Auth0/Clerk/Firebase issuer whose tokens the API accepts — was reachable over MCP. It required a full-access key, so it was never open to a read-only one, but it was the wrong thing to delegate to a machine credential at all: trusting an issuer grants standing access to everyone holding that issuer's tokens, and it outlives revoking the key that set it, so "revoke the leaked credential" would have stopped being a complete answer. It is now an administrator action only, through the admin UI or the API with a signed-in session. A full-access key can still read the configuration, so an agent can see how a workspace is set up without being able to change who is trusted.
- Reading that configuration now states its own requirement. It always needed a full-access key, but the check lived one layer down in the service rather than on the tool itself — so a later refactor could have widened it without anyone noticing. The rule is now enforced where the boundary is, with a test that fails if it moves.
Changed
- A read-only AI agent can now preview a migration. Asking "what would this schema change cost me?" used to require a full-access MCP key — the only kind that can also run the migration. That was backwards: the credential that cannot change anything is exactly the one that should be able to look first. The preview is now available to any key that can read the workspace. It stays a preview — nothing is applied, and a read-only key is still refused the moment it tries to make the change. The preview never revealed anything a read-only key couldn't already obtain (the column's definition from a normal read, the row count from any query), so this opens no new window onto your data.
- A view-scoped key can no longer preview a column outside its scope. The same change tightened the other direction: because a preview names the table's row count and the views a cascading foreign key would touch, it now requires access to the view the column belongs to. A key restricted to certain views previously could preview any column in the workspace.
2026-08-27
Added
- A table can now point at itself. Categories with a parent category, employees with a manager, threaded comments — self-referential relationships used to be read correctly on import but impossible to create from the relationship picker, and a lookup through one (
parent.parent.name) was refused by a cycle guard. The guard turned out never to be protecting anything: a lookup follows a stored, finite path, and the depth cap is what actually bounds it. It is gone. The picker now offers your view's own table (pinned first, labelled "this table"), multi-hop self-lookups resolve — a category row can show its parent and grandparent — and the automatic reverse relationship ("rows pointing at this one") is created without tripping over the forward one's name. Two edges to know: a self many-to-many is still not offered, because its join table would derive two identical column names, and the picker builds paths at most five hops deep.
Fixed
- Creating a new relationship could finish the database migration and then fail to register the relationship. Since the composite-key feature in April, the message that travels back after a relationship's migration succeeded carried derived fields that could not survive being read back — reading them either failed outright (the relationship was never registered, though the foreign-key column had been added to your database) or, for composite keys, silently doubled the column list. The derived fields no longer travel; the values they were computed from always did. Caught by rewriting a test that had been passing without executing anything. Fixing it uncovered a second gap behind it: even once the relationship registered, the visible column you asked for was never created — the foreign key and the relationship appeared, the "Adding column…" notice resolved, and the grid showed nothing new. The column is now created with the name and display field you chose, and a browser-driven end-to-end test walks the whole journey: picker → migration → the related row's value showing in the grid.
- The Zapier triggers now actually poll newest-first. The "New Row" and "Updated Row" triggers requested their sort as
field.desc— a syntax the API never understood, and until today silently ignored, so the triggers were really polling in primary-key ascending order the whole time (a new row past the first page could be missed until older rows aged out). Today's stricter API refused the malformed sort with a400, which is exactly how the bug finally surfaced — the first Zapier poll after the deploy. Fixed in version 1.5.2 of the Zapier integration; existing Zaps pick it up once migrated to that version. - A new relationship's foreign-key column no longer copies
SERIALfrom the key it references.SERIALis not a real type — it is an integer plus an auto-fill default — so a foreign-key column created asSERIALcame pre-filled on every existing row with a sequential id: each row silently "referencing" an arbitrary row, most often itself. The column is now created as the plain integer type underneath (integer,bigint,smallint), so existing rows start empty, the way an unset reference should. Rows auto-filled by the old behaviour are not rewritten — if you created a relationship on a table imported with serial keys, check those values before trusting them. - Linked records work on UUID-keyed tables. A many-to-many between tables with UUID primary keys was a trap: the chips displayed fine, and then opening the picker or toggling one failed, because every id was pushed through an integer parser. Ids are now read as whatever the key actually is — a number, a UUID, or text — on both sides of the link. Numeric keys behave exactly as before. Verified against real UUID-keyed tables on PostgreSQL; a MySQL table storing UUIDs as
CHAR(36)has not been exercised and may still refuse.
Added
- An import can now correct rows instead of duplicating them. Importing only ever added, so running the same file twice gave you everything twice — which made a corrected export something to clean up after rather than simply re-import. Name the columns that identify a row (
conflictColumns) and choose what happens when one already exists: keep the stored row and skip the incoming one, or overwrite its other columns. The columns you match on are never themselves rewritten, and skipped rows are counted separately from failures — a clean re-import of the same file should not look like it went wrong. Two things to know. The match columns need a unique index in your database: that is the only thing that lets a database recognise a duplicate, and without one the import is refused with a message saying so rather than silently duplicating. And on MySQL the match is broader than the columns you name — it reconciles on any unique key on the table, where PostgreSQL uses exactly the ones you gave. Available through the API today; the import dialog has no match-column picker yet, so a file imported through the app still only inserts.
2026-08-26
Added
- You can now see what an AI agent actually did. An MCP key used to leave two traces: a "last used" timestamp that every call overwrote, and a monthly request count. Neither answers the question people ask after handing a key to an agent — what did it read, and what did it change? Every tool call is now recorded: which credential, which tool, which view it named, and whether it was allowed. Refusals are recorded too, and they are usually the rows worth reading — a key repeatedly bouncing off a view it has no scope for is how a mis-issued key announces itself. Read it at
GET /api/workspaces/{uuid}/mcp-keys/activityas a workspace admin; a screen in the app will follow. Two deliberate limits: the arguments are not stored, only the identifier a call named, because a single create-record call carries a whole row and an audit table should not become a second copy of your data on our side; and a failure to write the record never fails the call it describes, so this is a strong record rather than a guaranteed one. - Two conditional validation rules now work. "If the account type is premium, a credit limit is required" and "if the membership level is gold, points must be between 1000 and 10000" — rules that only apply when another field says so. Both are enforced on every write path, the app and the generated REST API alike, and both are careful about the case that matters most: when the condition does not hold, the rule says nothing at all. The condition is matched as text, so a rule written against
truestill fires for a JSON boolean and one written againstPREMIUMstill fires forpremium— matching on types instead would make the rule quietly never fire, which looks exactly like a row that passed. These are not yet offered in the rule builder, because it collects two fields and a conditional rule needs a field, a value to match against, and a target; for now they can be created through the API or an AI agent, and the builder will follow. - Filters can say OR,
notInandbetween. Filtering was AND-only, so "paid or urgent" meant two requests and a merge on your side. Alternatives are now written as numbered groups —filter[or][0][status]=paid&filter[or][0][total.gte]=100&filter[or][1][priority]=urgent— where conditions inside a group are AND-ed, the groups are OR-ed with each other, and the whole thing is AND-ed with any plain filters outside it. Two ordinary operators arrived with it:filter[status.notIn]=archived,void, andfilter[age.between]=18,65, which is inclusive and wants exactly two bounds (one or three is a400rather than a guess). If your workspace uses row-level security, it is applied outside your groups — an OR in a query string can narrow what you see but can never reach another user's rows. A malformed group (no number, or a non-numeric one) is refused rather than ignored, because quietly dropping it would hand you back rows you meant to filter out.
Fixed
- All seventeen validation rules are now enforced by the generated REST API. Twelve of them were.
REQUIREDand the four date rules (PAST,FUTURE, and their or-present variants) were checked when a person typed in the grid, when a CSV was imported, and when an agent wrote through MCP — but fell straight through on the REST API, which is the path a program uses. That madeREQUIREDroughly useless where it matters most, because its whole purpose is to demand a value on every write while the column itself stays nullable. All five are enforced there now, with the same whole-day meaning for dates that the app already used. On update the rules behave the way partial updates need them to — a field your payload does not mention is left alone, but explicitly setting a required field to null or blank is refused. - Four kinds of bad request stopped being answered with a misleading success. A filter value that isn't valid for its column (
filter[age]=abc) used to return200with an empty list, which reads as "no rows match" — a statement about your data rather than about your query, and impossible to tell apart from a typo. An unknown sort field was silently ignored, so you got primary-key order under a200. A filter across a linked collection whose target had no text to search was dropped entirely, so a filter written to narrow a result quietly returned every row instead. And asking to expand a relationship that doesn't exist surfaced as a500, the API blaming itself for a typo in your query string. All four now answer400and name the field at fault, matching how an unknown filter field was always treated. Asking whether a linked collection is empty now works even when there is nothing searchable on the far side, which it previously did not. - MCP keys now enforce their access level and view scope on every tool — reads included. An MCP key connected to an agent is meant to be a narrow credential, but several of the tools behind it checked only that the object being touched belonged to the key's workspace, not that the key was allowed to touch it. That gap is closed across the board. A read-only key can no longer create, edit, fill, or bulk-change rows, and can no longer add or delete indexes, constraints, or relationship configuration — those all require a full-access key now. A key scoped to specific views is refused every other view's data, including simply reading it or listing a relationship column's options, rather than being reachable if you knew a view's identifier. Two workspace-boundary holes were also closed: an entity-level constraint and a filter preset can now only be changed or deleted through a key belonging to their own workspace, where before a key could reach another workspace's by identifier. Nothing changes for a properly-scoped key doing what it was issued to do; the fixes only remove reach a key was never meant to have. Verified by 15 new tests that drive the live MCP endpoint with real reduced and view-scoped keys, alongside the existing key-security suite.
Changed
- Three pieces of documentation that described things the product doesn't do have been corrected. The bulk operations page advertised an Excel (
.xlsx) export format — there is no spreadsheet writer and every export is a CSV, so the format table is gone and the page now says so plainly, along with two things it never mentioned (export headers are database column names, not the display names in your grid, and rows come out in primary-key order rather than your view's sort). For AI agents, two MCP tool descriptions were misleading in ways that would waste an agent's time:import_schemaandsync_schemaare the same operation under two names, which is now stated on both, andsync_view_columnsonly fills in views that have no columns — it cannot refresh a view whose columns have drifted, despite what the name suggests.
Added
- Four new posts on the blog. The widget is not the column type — how a field type sits on top of a plain database column, and why the currency or date formatting you see lives in the browser, so the API and exports return the raw value underneath. The export is a snapshot, not the exit — what a data export actually is when you already own the database it came from, and a candid list of its limits, including that the docs' promised Excel format does not exist and every export is really a CSV. The join table is a real table — many-to-many relationships as genuine join tables with their own columns you can drill through, and the three ways a related row can be edited. A key that can't see the whole workspace — the access levels and view scoping that narrow an MCP key, written alongside the enforcement fix above.
2026-08-24
Added
- Validation rules that relate two columns are now enforced on the API. Rules like "start date before end date" or "at least one of email and phone" used to be checked only when editing inside the app — a program writing through the REST API could create or update rows that broke them. They are now enforced on every API write: creating a row checks the whole payload, and updating a row checks the row as it will be stored, so a partial update cannot sneak a violation in through a field the rule also reads. Same rules, same messages, every writer.
- Real-time catch-up after a reconnect. Updates now carry an id, and a reconnecting browser tells the server the last one it saw; the server replays what was missed and the "you may have missed updates" banner clears itself. When the gap is longer than the replay window — a night's sleep, a server restart — the banner stays and offers a refresh instead, because a partial history presented as complete would be worse.
- Real-time collaboration is now tested. The four classes behind it had no tests at all, and the three end-to-end tests named after real-time each passed whether it worked or not. There are now 65, covering which connections an event reaches and which it must never reach, that an event belonging to no view is not sent to every view, that dead connections are cleaned up rather than accumulating, and who is allowed to open a stream in the first place. Each was checked by deliberately breaking the code it watches to confirm it fails — including opening two authorisation holes and confirming both are caught.
- A post on real-time collaboration. Someone else's keystroke — how an edit reaches the people who should see it and nobody else, why an event that belongs to no view is dropped rather than sent to every view, and what keeps a connection alive through a proxy. It is also candid about what prompted it. The broadcasters behind real-time had no tests, and the three end-to-end tests named after real-time all passed whether it worked or not — one checked for the absence of an error banner, which is also absent when nothing ever connected; one reloaded the page before checking the row had arrived; and one asserted a count was zero or more, which is true of every count. There are now 65 tests, and each behaviour was checked by breaking the code it watches on purpose to confirm the tests fail when it does.
- Three more posts on the blog. Rules the API can't skip on validation — why a constraint belongs on the column rather than in a form, and which of the two tiers the generated REST API actually enforces. One cell, five columns on display groups, including why the joined value never exists in SQL and what that buys you. The arrangement you keep rebuilding on presets, and why a preset is a way of looking rather than a permission boundary.
Fixed
- Edits made through the API were invisible to everyone watching the grid. Editing a cell, inserting a row, filling a column, batch-editing or changing linked records through the REST API saved correctly but never told anyone: the code that attributes the change to its author failed on a technicality after every API write, and the failure was caught and logged where nobody looks. Your own edits always looked live because your browser shows them optimistically — it was everyone else's screen that stayed still until refresh. Found the same day real-time collaboration got honest end-to-end tests, by those tests failing; fixed, and proven by the same tests passing.
- Row style colours are validated. The dialog always offered a safe palette, but the API accepted any string as a colour and applied it as-is — invisible in dark mode at worst. Colours must now be six-digit hex.
- Two organisations using the same workspace name no longer share a real-time channel. Workspace names became unique per organisation rather than globally, so two organisations can each have an "acme-store" — but the real-time channel was still addressed by the name alone, so two organisations sharing a name received each other's workspace-level notifications: views being created or deleted, member changes, key and configuration changes. Records were never affected — row and cell updates travel on a separate per-view channel with a globally unique address — and nobody gained access to anything. Fixed properly: every notification now carries the organisation it belongs to and is delivered only to that organisation's connections, so two organisations sharing a name each receive exactly their own updates, live. Forgetting the organisation is now a compile error on our side rather than a routing bug on yours. Found while writing tests for real-time collaboration, not through a report.
- Three cross-field constraint types could be chosen but never worked. A constraint whose validator does not exist is skipped, and a skipped constraint reads as a row that passed — so "Unique Composite" and "Conditional Required" accepted everything, and "Fields Not Equal" was sent under a name the API does not recognise. All three are corrected: the two without validators are no longer offered, and Fields Not Equal now works because it is sent under the right name. Two rules that always worked but were offered nowhere, Fields Equal and Exactly One Required, are now available. If you configured a rule of one of the removed types, it was not protecting anything — worth re-checking that data.
- The row styles page said style rules are saved inside filter presets. They are stored on the view, so everyone looking at that view sees the same styling and switching presets does not change it. The blog post that repeated the claim has been corrected too.
- The presets page documented three visibility levels. There are two. "Shared" does not distinguish workspace members from guest share-link holders — if someone can open the view, they can see the preset. Anyone who chose "Team" expecting guests to be excluded should re-check those presets.
- Filtering by a date failed on every "Created At" column. Adding a filter to a timestamp column that carries a time zone — which every automatic Created At and Updated At column does — replaced the grid with "'2026-08-17T00:00' is not a valid TIMESTAMPTZ value for this filter". The filter demanded a time zone offset written out in full, and no date picker produces one, so no value you could enter was acceptable. Dates are now read the way they are written: with an offset, without one, or as a plain date. A value with no zone is read as UTC.
- "On or before" a date excluded that whole day. Once a date filter was accepted, it was compared against midnight, so filtering "on or before 17 August" hid everything that happened during 17 August, and "is 17 August" matched only a row stamped exactly 00:00:00 — which no row ever is. A date now means the day: on or before includes all of it, after excludes all of it, and "is" matches any moment within it. Picking a date in a filter no longer asks for a time you did not want to specify; filtering to an exact instant is still available through the API.
- Conditional row style rules on a date column coloured nothing. A rule like "Created At — greater or equal — a date in August" saved without complaint and then never fired, whatever date you chose, because the comparison was being done on the text of the stored timestamp: "2026-08-20T09:15" sorts before "8/17/2026" for the same reason "2" sorts before "8". Dates, times and timestamps are now compared as moments, on the same whole-day basis as the filters above, and the value box offers a date picker so a rule and a filter written the same way now agree. Existing rules that hold a hand-typed date need the date re-picked — 8/17/2026 and 17/8/2026 are the same day written by different people, and guessing which half is the month would silently colour by the wrong day.
- Several column types were being compared as the wrong kind of thing. In row style rules: a file or image condition could never match, because the cell holds a file reference and the rule was comparing against the words "[object Object]"; a rule naming a dropdown option by the label shown in the grid did not match the value stored underneath it; whole-number columns that Postgres reports as
smallintwere compared as text, so 10 was "less than" 9; and a truth value written astorymeant false here while meaning true on the server. In filters: a price column was compared through a rounded floating-point value, so "equals 19.99" could miss the row holding 19.99;double precisionandrealcolumns were sent as text and refused the query outright; and a filter on auuidcolumn failed for the same reason. All corrected, and each one now has a test naming the failure it replaced.
2026-08-23
Added
- Row styles can set the text colour, not just the background. The setting existed in the data model and the grid already knew how to draw it, but the dialog never had a control for it, so it was unreachable unless you called the API directly. There are now eight text colours beside the eight backgrounds, each shown as the letter A in the colour it applies. Like the backgrounds they carry a separate light and dark value rather than a single fixed one, so a rule set up in daylight still reads at night — and each was checked for contrast against white, against its own pastel background and against the dark surface, so no combination of the two palettes produces text you cannot read.
- A post on conditional row styles. Why your green rule never fires — why two overlapping rules give you the wrong colour unless you order them narrowest first, what a condition can target (including formula columns, which compare as numbers even when they don't declare a numeric type, and relationship columns, which compare the label you can read rather than the key underneath it), and the four places the in-grid styling engine quietly disagrees with the same filter sent to the API. Written after the two files that decide every row's colour turned out to have no tests between them; they have 49 now.
- A post on owning the list rather than renting it. The list that isn't in your email tool — why signups are better held in a database you own, with the email tool downstream of it rather than in charge of it, and what that changes the day you switch providers.
- A post on formula columns. The column that isn't there — writing an expression gives you a column that is computed on every query and stored nowhere, so nothing can go stale and nothing needs migrating. It covers what you can write, why the validator masks string literals before checking them, how aggregating across a relationship works, and that every schema sync used to delete these columns until two days ago.
- A post on relationship columns. The other table's data, without writing the join — how a foreign key becomes the related row's name, a count of the rows on the far side, or several values at once, and what happens when a lookup path is wrong. It also admits that filtering a relationship column was returning an error until two days ago.
- Two posts on the Zapier integration. Your database, wired to 7,000 apps covers what the integration does — triggers when a row is added or changed, actions to create and update rows, a search — and is honest that nothing is copied anywhere: every trigger reads and every action writes the database you already own. Polling is not real-time explains why a change appears in the grid instantly and in a Zap some minutes later, what a poll actually reads, and which tool to reach for when that difference matters.
Fixed
- A conditional style rule with an empty comparison value coloured rows anyway. A style rule is saved as soon as you pick a column, so a rule you had not finished writing was already being evaluated against every row. With the value left blank, equals matched every row holding a zero and greater than matched every row holding any number at all — an empty value was being read as the number zero. An unfinished condition now matches nothing, which is the safer of the two options when the result is something you can see. Found by writing the tests for the post above.
- The Zapier connection screen offered help that went nowhere. All three links beside the API key and slug fields pointed at a domain that does not resolve, and one of them at a page that has never existed — so anyone setting up the integration was sent nowhere at exactly the point they needed instructions. One had been broken since the production host changed months earlier. Fixed and published as integration version 1.5.1. Found because writing the post above meant setting the integration up the way a new user would.
Changed
- Fewer third-party requests from your browser, and crash reports no longer leave our domain. The app was loading a performance-monitoring SDK that sent data to a third-party host on every page. It has been removed — it never produced anything usable, because the host is on tracker blocklists and most of our users run one. Error reports, which are worth keeping, now travel through
schemastack.ioinstead of a third-party domain: same information, one fewer party your browser talks to, and it works whether or not you block trackers. Analytics on the public site has worked this way for a while; the app's error reporting simply had not caught up.
Fixed
- Crash reports were arriving unreadable, when they arrived at all. Two separate problems. Reports from anyone running tracker protection never left the browser at all, which in practice meant almost none of them did — the project had not received a single report since it was set up. And those that would have arrived pointed into minified code, naming a line in a bundled file rather than the code that failed, because the build published no source maps. Both fixed: reports are sent through our own domain, and readable positions are uploaded at build time. The maps themselves are never published, so nothing extra is served to you and the source stays private.
- Real-time updates did not come back after a laptop had been asleep. Leaving a workspace open overnight left "Reconnecting to real-time updates…" on screen and nothing arriving. Three separate faults, all needing a gap in time to appear, which is why none of them showed up in ordinary use. The connection dies during sleep without reporting an error and without any timer running, so the app still believed it was connected and every reconnect trigger skipped — it now treats a connection that has been silent longer than the heartbeat window as dead regardless of what it thinks. The access token expires during the night, and the streams bypass the layer that normally notices that and refreshes, so every retry re-sent the same dead token forever — a stream now refreshes on the spot, and stops with a clear state if the session is genuinely over. And a wake raises two reconnect triggers at once, so a second attempt would start while the first was still connecting, and the older one would then close the newer one and start the cycle again — attempts are now identified so an outdated one cannot interfere.
2026-08-22
Fixed
- Row-level security was not enforced when reading a single record by id. Fetching one row by its id does not pass through the filter that narrows list queries, so a separate check confirms the row belongs to the caller. That check looked the value up by the database column name — but generated entities expose columns under their field name, so
tenant_idis reached throughgetTenantId. For every column whose names differ, which is every snake_case column, the check found no method and allowed the row through. An end-user of an application using external identity could therefore read any row whose id they could guess, including other users' rows. The check now resolves the field name from the entity's own metadata, and refuses the row when it cannot establish that the row matches instead of allowing it. Both halves matter: the lookup makes the check work, and failing closed means a future mismatch is a refusal rather than an opening. - Filtering the grid by a relationship column returned an error. The value was converted to the type the cell displays — the related row's label, so text — while the filter is applied to the numeric key underneath, and the database refused the mismatch. What you saw was "An unexpected error occurred" with nothing naming the problem. Fixed, along with a second case in the same place: a value that cannot be used for a column at all, like a word in a filter on a number, also produced that error. It now tells you which value and which type it could not use. Every column type and every operator is now covered by tests — 90 of them — because a filter that fails on one type and works on another is the kind of thing that only shows up when somebody hits it.
- Row-level security never worked on a normally named column, and did not constrain writes. A policy is written against a database column —
owner_id— while everything that consumes it speaks the entity's field name —ownerId. Because the two were never translated, three things were broken at once. Listing records answered a server error rather than a filtered list. Creating a record looked for the policy's column in the request body, never found it, and let a caller set that field to whatever they liked. Updating a record likewise never noticed the field, so it could be changed to another user's value. Reading one record by id was the case fixed earlier the same day. The translation now happens once, and a policy naming a column the entity does not have is refused rather than quietly producing an unfiltered query. If you have row-level security configured, it is worth re-testing now that it works — and worth knowing that until today, listing was failing loudly rather than leaking. - Any organisation member could change the organisation's subscription tier. The code documented itself as owner/admin only and enforced nothing. It never reached another organisation — the organisation comes from the caller's own token — but it let a member act above their level. Now restricted to owners and admins.
- An MCP tool asked for six fields that do not exist.
update_entity_api_configadvertisedenabled,allowRead,allowCreate,allowUpdate,allowDeleteandmaxPageSize. The configuration has none of those — it has default expansion, expansion depth, default fields, expandable relationships, filterable fields and a rate limit. Unknown fields are silently ignored, so an agent following the description sent six values, changed nothing, and was told it succeeded. The description now names the real fields. - The filtering reference documented an error that does not happen. A filter value that cannot be converted to the column's type was described as returning
400; it actually returns200with an empty result. The page now says so, and flags it as a defect rather than a feature. A filter on an unknown field does correctly return400— that part was accurate. - Database views were listed as not imported. They are imported and readable, just not editable, and drift checking treats them as tables. Moved to partially supported with the real behaviour described.
Changed
- Two blog posts corrected. The REST API you didn't write and Everything you can ask the API for both showed filter syntax as
filter[field][operator]=value. The real syntax isfilter[field.operator]=value, and the bracket form returns400. The second post also listed a misspelled operator as failing silently; it returns400too. Corrected, and the verification note on that post now records that its examples were run against a live API rather than inferred from the query-layer tests.
Added
- Four more posts on the blog, and every topic now has one. Your spreadsheet, in a real database on CSV import and what happens to the rows that don't fit; Everything you can ask the API for, the full query surface including the three places it guesses where it should refuse; Sometimes the right answer is no on estimating what a migration will lock before running it; and Half our readers aren't people on why every page here has a Markdown twin.
Fixed
- The CSV import size limit is now enforced, not just documented. The import guide said files up to 100 MB were supported and nothing checked it, so a larger file would begin parsing and fail somewhere in the middle. The file is now refused when you choose it, with its size and the limit in the message. Found while fact-checking the blog post about CSV import, which is the point of writing them that way.
2026-08-21
Improved
- Connecting an AI client no longer starts with hunting for a workspace UUID. Give Claude, ChatGPT or any MCP client
https://schemastack.io/mcp— nothing after it — and the consent screen now lists the workspaces you can reach and lets you pick one. Naming the workspace in the URL still works and still skips the list, which is what you want when writing setup instructions for other people. The list is only ever what you could already open: an organisation owner or admin sees every workspace in the organisation, everyone else sees the ones they are a member of, and a workspace in maintenance mode is offered to administrators only. Picking from the list grants nothing by itself — the choice goes back through exactly the same check as a workspace named by the client, so it is who decides that changed, not what gets verified.
Fixed
- Workspace storage settings were readable by anyone with an account, and couldn't be saved by anybody. The four S3 storage-configuration endpoints documented themselves as owner/admin only and enforced nothing, so any signed-in user who knew a workspace's ID could read another workspace's storage settings — bucket, region, endpoint and access key ID, though never the secret, which is write-only. The same endpoints now require workspace administrator access. Saving a configuration was separately broken for everyone: every attempt failed with a server error, which is why no workspace in this deployment has ever had one saved. Both had the same cause. The save path reloads the workspace immediately after writing, and without a permission check nothing had loaded the workspace's organisation first, which made the database layer fail on the reload. The missing check was hiding a broken endpoint, and the broken endpoint was hiding the missing check. Fixed independently of each other, so neither depends on the other to work.
- A consent flow that could have handed out access to the wrong workspace. When an administrator registers an OAuth2 application, the application carries the workspace it was registered for — and consent took that as settling the whole question, never checking whether the person signing in could reach that workspace at all. Anyone with an account and a client ID could therefore have approved a connection to a workspace they had nothing to do with and been handed a working token for it; client IDs are not secrets. Nobody was affected: no OAuth2 application had ever been registered in this deployment, so no authorization code and no token was ever issued through the affected path, and there is nothing for you to check or revoke. We found it while building the workspace picker above, confirmed the defect end to end against a test database, and confirmed against production that it was never reachable there. Fixed: consent now verifies the signed-in person's access on both paths, not just the one where the workspace is named in the request. Applications that register themselves — the route the AI connector uses — were never affected, because that path always checked.
- An AI agent with read-only access could reset a workspace's schema. Four MCP tools that rebuild schema metadata — reset, import, sync and view-column sync — checked that the credential was valid but never checked what it was allowed to do. Any enabled key could run them, including read-only ones, and including the OAuth tokens that are capped below schema level precisely so this can't happen. They now require full access, and a workspace that is locked or read-only refuses them outright. Nobody's data was ever at risk — these tools rewrite SchemaStack's metadata, not your tables — but view configuration, validation rules and formula columns were. Reading drift is still allowed at read-only, because looking changes nothing.
- Schema sync was deleting formula columns. Computed columns exist only in SchemaStack — there is no database column behind them — and the documentation said sync preserves them. Two of the three code paths agreed; the one that decides what to delete did not, so every sync quietly removed them along with their validation rules. Fixed.
- Imported database views reported drift forever. Importing reads tables and views; drift checking read only tables, so any imported view looked permanently "removed from the database" and the quick check never came back clean. All three now agree on what they are looking at.
- CSV import now validates every row, and tells you which one failed. Imported rows were written without the validation a typed-in cell gets: required fields, maximum lengths, email formats and cross-field rules all went unchecked until the database itself objected. And because rows were written 500 at a time in one statement, a single bad row failed all 500 and the error blamed the first row of the batch. Each row is now validated first, rejected rows are reported individually with their row number and skipped, and the rest of the file imports.
2026-08-20
Added
- SchemaStack has a blog — and everything on it is verified before it publishes. One feature per post at schemastack.io/blog, each carrying a public stamp saying when its claims were last run against the real product and by what — a test suite, the production synthetics, or a hand-run session. The build refuses to publish a post without that trail. Four posts at launch: the agent connector, the spreadsheet that builds your schema, guest links and external identity with row-level security. There's an RSS feed, and every post is also published as Markdown for AI assistants, like the rest of the site.
- Add SchemaStack to Claude or ChatGPT with a URL — hosted AI clients have nowhere to paste an API key, so they sign in instead. Give the connector
https://schemastack.io/mcp?workspace=<uuid>and the rest happens on its own: the endpoint answers an unauthenticated request with a pointer to its discovery documents, the client registers itself, your browser opens the SchemaStack consent screen, and approving hands it an access token. Nothing to configure and no key to copy. The consent screen names the application, the workspace and what it is asking for — and says plainly when an application registered itself rather than being added by an administrator, because anyone can register under any name. - MCP accepts an OAuth 2.0 access token, not just an API key — an application acting for a person who signs in can now drive MCP with the same access token the Workspace API takes, instead of needing a workspace key of its own. Point it at
https://schemastack.io/mcp?workspace=<uuid>; the token already says which workspace it is for, and if the parameter disagrees the request is refused, so a token for one workspace cannot be aimed at another. An access token is capped lower than a key on purpose:workspace:readgrants Read-Only andworkspace:writegrants Data-Only, never Full — consenting to a scope called "write" means letting an application write records, not letting it drop a column on a database you own. Schema changes still require anmcp_key. The workspace access level remains the ceiling, and disabling an OAuth2 client now cuts off its MCP access immediately rather than leaving its live tokens working until they expire. - A machine-readable catalog of the programmable surfaces — /.well-known/api-catalog lists the Workspace API and the MCP server in the format described by RFC 9727, each with its documentation, its credential guide and a health endpoint. For the Workspace API it points at the OpenAPI 3.0 description, which every workspace has been publishing all along at
/api/v1/{orgSlug}/{workspaceSlug}/_openapi— generated from that workspace's own schema, no credential required, and now discoverable rather than something you had to be told about. The catalog is announced in aLinkheader on every page of the site.
Improved
- The website now says what SchemaStack actually is — the homepage led with data ownership and left the two things people choose SchemaStack for in the small print. It now leads with the point: using the spreadsheet builds real SQL schema, with previewed migrations, in a database you own. The AI-agent connector shipped this week has its own section, agencies get one describing client delivery (organisation per client, guest links, the client's own login provider with row-level security), and an honest feature-for-feature table compares SchemaStack with Airtable, NocoDB/Baserow and Directus at a glance.
- A Directus comparison page — Directus is the closest thing to SchemaStack and the comparison people most deserve to read, so it now exists: candid about Directus's maturity, GraphQL and automation flows, and clear about where SchemaStack differs — schema built from the spreadsheet with migration dry-runs, and agent access with consent and instant revocation.
- Revoking an application's access now takes effect immediately — access tokens are not stored anywhere, so revoking a grant used to stop the next token being issued while the one the application already held kept working until it expired, up to an hour. Revoking now records the moment it happened, and a token from before that moment is refused on both MCP and the Workspace API. This closes the gap for applications that added themselves, which a workspace administrator cannot disable because the application does not belong to their workspace. Reusing a stolen refresh token cuts off its access tokens the same way.
- An MCP request without a credential is now refused outright — it used to answer
200with an error inside the response, which also meant the full tool list was readable by anyone who asked. It now returns401with a pointer to how to authenticate, which is both what the protocol expects and one less thing given away. - Revoking an application's access no longer reaches other workspaces — with applications that belong to no single workspace, "revoke sessions" would have cut off that application everywhere it was used. Revoking now affects only the workspace whose administrator asked.
Fixed
- Signing in with an application that has no workspace of its own now works — exchanging an authorization code, and later refreshing it, read the organisation of the workspace being granted. That detail used to arrive by luck: it was loaded on the way to the application's own workspace, which was always the same one. For an application that registered itself there is no such workspace, so nothing loaded it and the exchange failed outright. Found by testing the flow end to end rather than in pieces.
- OAuth 2.0 was never documented — the Workspace API reference covered API keys and external identity providers, but not the OAuth flow, despite it having shipped. API authentication now covers registering an application, the endpoints, what each scope grants on the API and on MCP, and how to revoke access. The roadmap still listed it as planned; that entry is gone.
- The MCP security notes described the wrong credential — the AI integration guide said MCP authenticated with "the same JWT token you use for the REST API" and that your account roles governed what an assistant could do. Both predated MCP API keys: access is granted by an
mcp_key or an OAuth token, a browser session token is not accepted at all, and the workspace MCP access level is the ceiling. The page now says so, and warns that a key is a workspace credential rather than a personal one.
2026-08-19
Added
- The public site answers in Markdown — every page on schemastack.io now has a Markdown twin: append
.mdto the path, or sendAccept: text/markdown. A new llms.txt indexes every page, the documentation and the machine interfaces in one file. An AI assistant asked about SchemaStack no longer has to reconstruct the answer from a marketing page's HTML — and the FAQ answers, which are collapsed in the browser and therefore absent from the HTML, are included in full. - The documentation is published as Markdown too — every page on docs.schemastack.io is now available at its own URL plus
.md, indexed in llms.txt, and available in full as a single file at llms-full.txt. VitePress rendered the Markdown to HTML and discarded it; since the source is already the format AI assistants want, it is now published alongside. Links between pages point at the Markdown versions, so an assistant following a reference stays in Markdown rather than being dropped back into HTML. - Credential instructions for AI agents — /.well-known/auth.md states which credential each surface takes (an
mcp_key for MCP, ansk_live_key or OAuth 2.0 token for the Workspace API), that none of them are self-service, and that MCP access is disabled per workspace until an administrator raises it. An agent that reads it first stops retrying a403that will never succeed. - Content signals in robots.txt — the public site now declares explicitly that automated systems may index it, cite it and train on it. The declaration covers only what the site serves; customer data lives in the customer's own database and was never reachable from here.
Fixed
- Unknown addresses return a real 404 — every unmatched path answered with the landing page and a success status, so a mistyped URL looked like a working page, and crawlers read the site as having a large number of duplicate homepages. Unmatched paths now return a proper 404 page. Every linked page and every app route is unaffected.
Improved
- API and MCP responses point at their own documentation — responses from
/api/*and/mcpcarry aLinkheader naming the relevant reference page and the credential guide, so a client that lands on an error has somewhere to go without a web search. - The landing page FAQ is a proper disclosure — each question now reports whether its answer is open, so a screen reader announces the state instead of leaving it implicit in a rotating chevron.
2026-08-17
Added
- Create another organisation from the UI — the option existed in the API but was unreachable: the page redirected you to the dashboard once you already belonged to an organisation. It now lives at the bottom of the organisation switcher in the toolbar, and that switcher is visible even when you only have one organisation.
Fixed
- Workspace names no longer collide with other people's — workspace addresses are checked per organisation, but the database required them to be unique across every organisation. So a name could be confirmed as available and then fail with "An unexpected error occurred", because an organisation you cannot see had already used it.
- Workspaces no longer hold database connections after being deleted — a deleted workspace kept its connection pool open indefinitely. On a managed database, which is shared across an organisation and capped at five connections, a few deletions could exhaust it and leave the remaining workspaces unable to load data.
- A workspace at its connection limit now says so — it previously surfaced as "An unexpected error occurred". The API returns a clear, retryable error instead.
- Two workspaces can share a managed database again — connection pools were sized larger than the database itself allowed, so a second workspace on the same managed database was always refused.
2026-08-16
Fixed
- Resending a workspace invitation works again — the Resend button on a pending member in Workspace → Members called an endpoint that did not exist and always failed. Resending from Organisation settings was unaffected.
- Invitation links no longer point at an unreachable address — the link returned when inviting someone was built from the server's internal bind address, producing
https://0.0.0.0:8443/.... The link in the invitation email was always correct; only the API response was affected.
Removed
- Invitation links are no longer returned in API responses. The link contains the invitation token, which is the proof that someone controls the invited email address — and for an invitee who does not yet have an account, that token is enough to create the account and set its password. Returning it to whoever sent the invitation defeated the email check. The token now only ever reaches the invitee by email.
2026-08-15
Added
- Paste a connection string when connecting a database — the database form now takes the connection string your provider gave you and fills in the fields itself: host, port, database, username, password and SSL mode. Connection URIs (
postgresql://…,mysql://…), JDBC URLs andlibpqkeyword form (host=… dbname=…) are all understood, and any parameters we don't recognise are kept rather than dropped. Available both when creating a workspace and when editing its database settings. - Number and date display formats in the properties panel — the formatting options that previously had to be set through the API are now editable in the UI: number format (currency, percent or grouped number) with decimals, prefix and suffix on Integer and Decimal columns, and a date format on Date columns.
Improved
- Display options sit with the other display settings — casing, prefix/suffix, number and date formats and the Select options editor have moved out of the Schema Settings section, which warns that changes need confirmation, and into Display Settings, where they belong: they save immediately and never touch your database. The formatting controls are tucked behind an "Advanced" toggle, since most columns never need them.
- MCP setup instructions now work — the connection guide gave a placeholder hostname and asked for the wrong kind of credential, so following it exactly could not succeed. It now shows the real endpoint, the
mcp_API key (created from Workspace → MCP in the admin app) and the full list of 57 tools, up from the 18 previously documented. - Connecting your database has its own guide — a new page covering the accepted connection string formats, what each field maps to, how
sslmodetranslates, where to find the string for common providers, and why a direct connection is preferable to a pooled one.
2026-08-14
Added
- Preview column changes before applying them (API) — a new
preview_column_changeMCP tool answers "would this change require a database migration?" without touching anything: metadata-only changes returnmigrationRequired: false, schema-affecting ones return the full impact analysis (estimated duration, row count, read/write blocking, SQL preview). - Conditional formulas — computed columns can now use standard SQL
CASE WHEN … THEN … ELSE … ENDexpressions, the vendor-portable conditional (previously only MySQL'sIF()worked). Formulas are validated structurally (balanced CASE/END, no stray fragments) and against your own database before saving. This makes computed columns the full answer for custom display transforms — e.g.CASE WHEN maiden LIKE '%née%' THEN maiden ELSE CONCAT('(née ', maiden, ')') END— evaluated server-side so the grid, API, exports, and sorting all agree. - Select options editor — columns using the Select widget now have an Options section in the column properties panel: add, edit, and remove value/label pairs directly in the UI.
- Date display formats — date columns can render as "16 Apr 2014" (medium) or "04-16-2014" (US) via a widget option, instead of the raw stored value. Display-only.
- Number display formats — decimal and integer columns can carry a display format in their widget options: currency (thousands grouping,
10,300.00), percent (14%), or a generic number format with configurable decimals, prefix, and suffix. Formatting is display-only — stored values and editing are unchanged. - Composite cells via display groups (API) — columns can be grouped into one cell (e.g. invoice number + date) by setting the same
displayGroupthrough the column-update API; grouping was previously only reachable from the UI. - Aggregate columns — a column can now summarize related records: count them, sum/average a field, or join them into one cell with a custom per-record expression, a row filter, and a separator. Example: "all sent invoices, formatted like
APT-00017/2025 3 Mar 2025, newest first, separated by semicolons" — computed in your database by a server-generated query, so the API, exports, and grid all agree. - Required fields without database changes — a new REQUIRED constraint makes a field mandatory on every write (UI, API, MCP) while the database column stays nullable. Useful when legacy data predates the rule or the column carries a database default: new writes must provide the value, old rows stay valid, and no migration runs.
- Form visibility separate from table visibility — each column now has a "Form visibility" setting (Same as table / Always in forms / Never in forms), so a field can be hidden in the grid but still editable in the create/edit form — or shown in the grid but kept out of forms.
- Cross-entity formulas — computed columns can reference a directly related record's fields (
category.name,customer.discount) and the value is joined in server-side. One relationship hop, to-one relationships; collections still use relationship display columns. - Text display options — text columns can render with a casing transform (UPPERCASE / lowercase / Title Case) and a prefix/suffix, configured in the column properties panel. Display-only: stored values and editing are unchanged; multi-value relationship chips keep their raw labels.
- Column list search and paging (API) —
list_columnsacceptsnameFilter,limit/offset(with a total-count envelope), andcompact=truefor a slim per-column summary — a 130 KB response on a large view becomes under 1 KB. - Rename a column's API key — the key a column uses in API payloads (
dataKey) can now be changed explicitly, e.g. to replace an imported hash name likefk9fdd0d…VenuewithassessmentVenue. Saved filters, presets, and style rules are unaffected (they reference columns internally by ID); external API consumers see the new key.
Improved
- AI message limits now persist — the daily AI chat allowance per member is counted from stored chat history, so it survives service restarts and applies consistently. When the limit is reached, the API returns a clear message with your limit — or connect your own AI via MCP for unlimited use.
- Grouped cells show formatted values — columns merged into one cell (display groups) now render each member through its display options: select labels, Yes/No, currency, and date formats apply inside the group ("Yes, 14 Mar 2025" instead of "1, 2025-03-03").
- Formulas can contain any text in quotes — string literals like
'SA Price with surcharge'no longer trip the SQL-keyword safety check (words like "with" or "set" inside quotes were wrongly rejected); unterminated quotes are now caught with a clear error. - Readable relationship names on import — schemas whose foreign-key constraints carry auto-generated names (Doctrine
FK_<hash>, MySQL*_ibfk_N, …) now get relationships named after the join column (enrolment_id→enrolment) instead of the hash, so API field names come out readable from day one. - Validation errors are readable over the API — error messages now list the actual violations ("vatPercentage: VAT percentage is required") instead of a bare "HTTP 400 Bad Request".
Fixed
- Invitations must be accepted before access — an invited member could previously reach organisation data before accepting the invitation; only active memberships grant access now.
- Editing cells on views with linked columns works again — views showing columns from related tables (via relationships) rejected every cell edit with false "field is required" errors for the related fields. Related display columns are no longer validated as if they were part of the row being written.
- Columns with database defaults are validated again — a column with a database default previously skipped all validation rules, so out-of-range values slipped in and clearing the field silently stored NULL (database defaults don't re-apply on updates).
- Computed columns keep their settings when updated — updating a formula column dropped display group, position, and other display settings from the request; they now persist like on any other column.
- Per-column role permissions are enforced on cell edits — explicit column-level write permissions were only checked for bulk actions; single cell edits now respect them too.
2026-08-13
Added
- Select fields — a column can now present a fixed set of labeled options. Configure
widgetOptions.options(value + label pairs) on a column and the grid shows the label ("African" instead ofafrican), while cell editing and the Add Row form offer a dropdown. Selects are purely presentational: they work on top of text, integer, and yes/no columns without changing the column's database type — switching an existing column to a select never triggers a migration.
Fixed
- Formula columns now work on MySQL workspaces — the create-time validation probe sent PostgreSQL-quoted SQL to every database vendor, so MySQL rejected every formula regardless of content (with an error confusingly labeled as coming from PostgreSQL). The probe now quotes identifiers per vendor and error messages name "the database" instead.
- Formula columns that return text are no longer mis-detected as yes/no — an expression like
IF(year <= 2020, CONCAT('APC', id), CONCAT('apt', id))was classified as boolean because it contains a comparison, and every value came back as "false". Comparisons inside a function call are now recognized as condition arguments, not the result. - Changing a column's widget no longer corrupts its stored metadata — switching widgets applied the new widget's create-time storage defaults (type, length, nullable) to the existing column's metadata, silently desyncing it from the real database schema. Storage defaults now apply only when creating a column.
- Yes/no detection at import is now based on your data — MySQL reports
tinyint(1)columns as boolean even when they hold values like months (1–12). Import now samples each boolean-looking column and keeps it numeric when out-of-range values exist, so a checkbox can never overwrite real data with 0/1. - Schema drift checks work over MCP/API keys — the quick and full drift checks crashed with an internal threading error when called through an API key.
- Row color rules can be saved through the API — the update-view API silently ignored the
styleRulesfield; rules saved through it now persist. - Workspace responses include their views again — API and MCP workspace lookups with
includeViewsalways returned an empty list even though the views were loaded. - Schema changes work over MCP keys — any column change that needed a migration crashed with an authentication threading error when requested through an MCP key.
Improved
- Clear warnings when MCP access is off — creating an MCP key while the workspace's MCP access level is Disabled now warns in the key dialog (before and after creation) and shows a banner over the key list, instead of silently producing a key that every call rejects.
- API tool documentation matches reality — several MCP tool descriptions had drifted from the actual accepted fields (bulk selection modes, column position and select options, view style rules, preset column overrides). All are now in sync.
2026-06-14
Added
- Public demo workspace at schemastack.io — the marketing site now links straight into a fully editable demo. Sign in with
demo@schemastack.io/demodemo(or use the "Try our live Acme Store demo →" button to skip typing the email). You land in the same editor the real product uses, with every affordance visible (Add Row, cell edit, properties panel, filter presets, import mappings). Attempts to save are intercepted with a friendly "Demo workspace — changes aren't saved. Sign up at schemastack.io to create your own workspace." snackbar instead of a generic permission error. Curators editing the demo from a separate non-demo account are unaffected; their changes go live for visitors on the next reload.
Improved
- Closed write-side gaps for read-only-by-identity users — three service-layer write paths that previously had no permission check beyond org membership are now gated: creating/updating/deleting filter presets, creating/updating/deleting CSV import mappings, and adding/removing/changing workspace members (the last as defense-in-depth — the org-admin check already blocked non-admins). For normal users this is invisible; it only matters for accounts the backend treats as read-only.
2026-05-13
Fixed
- A second formula column on the same view no longer disappears from query results — adding two formula columns whose display names would map to the same internal field name (e.g. "Margin %" and "Margin", both lowercased to
margin) caused the second one to silently vanish from row data. The database-key disambiguator was renaming the second column's row-JSON key (margin→margin_2) but the SQL builder was still using the underlying column's field name (margin) for the SELECT alias, so PostgreSQL produced two columns both aliasedmargin— one shadowed the other in the row JSON and anyORDER BYover them was reported as ambiguous. The SQL builder now uses the disambiguateddataKeydirectly. - Boolean formula columns are no longer mis-detected as decimal — a formula like
status = 'pending' AND ordered_at < CURRENT_DATE - 3is a boolean expression, but the formula-type heuristic only looked at arithmetic operators, saw the-inCURRENT_DATE - 3, and classified the whole column as a number (rendered with a DECIMAL widget). It now checks for comparison operators andAND/OR/NOTkeywords before arithmetic, so the same formula is correctly inferred asBOOLEAN.
2026-05-12
Fixed
- Read-only members can now drag columns to reorder them — viewer-role users couldn't drag column headers in a view, even though column order is purely a per-session UI state that isn't sent to the server (the only way order ever persists is by saving a preset, which already has its own permission check). The drag was being gated by the same workspace-write check that gates real schema changes. Drag now works for every role; the persistence steps (saving as the view default, or saving as a shared preset) keep their existing role checks. View-tab reordering is still admin/editor only — that one does write to the workspace.
- Read-only members no longer see write affordances they can't actually use — until now, viewers (read-only members) saw the Add Row button, the cell-edit popover, the bulk-edit / bulk-delete bar, and the full column- and view-management menus. Clicking any of them would either silently do nothing or surface a generic permission error from the server. The UI is now role-aware: data-write affordances (Add Row, bulk edit/delete, cell edit) are hidden for read-only members; schema-management affordances (column properties / rename / delete / hide, view rename / duplicate / delete, add column, row styles) are hidden for everyone below admin. Read-only members still see the data, sort columns, drag them locally, save private presets, and export — i.e. everything that doesn't write to the workspace.
2026-05-11
Improved
- Formula columns are now validated against PostgreSQL before they're saved — when you create a computed/formula column, the platform now (a) blocks a few more SQL constructs in the regex check (
CASE WHEN,ARRAY[…], square brackets,VALUES, unbalanced parentheses), and (b) round-trips the formula through your workspace's own database viaEXPLAINso any formula PostgreSQL won't accept is rejected at create time with the database's own error message attached. Previously, a malformed formula could pass the regex but produce invalid SQL downstream, breaking every read on the table until each formula column on that table was removed. - Workspace API now invalidates its metadata cache on every column / view / relationship change — workspace-api caches each workspace's entity model (up to 30 seconds for raw metadata, up to 60 minutes for the dynamic Hibernate SessionFactory) to avoid round-tripping to the metadata database on every request. Until now there was no signal from the metadata service to invalidate either cache on change, so a deleted/renamed column could remain in the projection for up to 30 seconds — long enough to leave a view's reads broken after a bad-formula incident. workspace-api now subscribes to the existing
workspace-eventsRabbitMQ exchange and invalidates both caches immediately when a column/view/relationship event arrives. - A failing formula no longer breaks the entire view's reads — formula columns are evaluated by a single projection query alongside the rest of the row read. If that projection throws (e.g. a formula references a column that's been renamed), the formula columns now return
nullfor that read and the rest of the row comes through normally, instead of the whole API call failing with500. - Formula columns that call any SQL function outside a small allowlist no longer break reads on the view — the formula rewriter qualifies bare identifiers as base-table columns (so
ratingbecomes"review"."rating"). It used a hardcoded keyword allowlist to skip function names likeCOALESCE/CONCAT/ROUND, but every other function —REPEAT,LPAD,RPAD,REGEXP_REPLACE, and so on — got the same column treatment, producing"review"."repeat"(...)and a PostgreSQLschema "review" does not existerror. The rewriter now detects a function call by checking whether the identifier is immediately followed by(, so any SQL function works without needing to extend the allowlist. - Smaller queries on views with formula columns — internal cleanup. The formula path used to pre-create
LEFT JOINs for every M2O/O2O relationship on the entity the moment any formula column was present, even if the formula didn't reference any of them. JOINs are now created on demand, only for relationships the formula actually traverses. - Clearer error response for syntax-level database errors — when a database call returns a PostgreSQL 42xxx error (syntax error, undefined column, undefined function, etc.), the API now responds with
400 Bad Requestcarrying the originalsqlStateand PostgreSQL message, instead of a generic500with "A database error occurred."
2026-05-02
Fixed
- Composite-primary-key tables stop responding after some hours of uptime — the workspace API caches a runtime-built data model per workspace, and the model is rebuilt when the cache evicts an entry (typically after periods of inactivity). For tables with multi-column primary keys, the rebuild collided with leftover state from the previous build and started returning 500 for every subsequent request to that workspace. Fixed by giving each rebuild fresh, isolated state. Composite-PK tables now stay responsive across cache evictions, schema syncs, and long uptimes.
Improved
- Workspace API backend errors are now forwarded to the error tracker — previously, unhandled exceptions in the workspace API (the service backing every CRUD request) were logged locally but never sent to error tracking, so we couldn't see them without manually checking server logs. They now flow through the same error tracker as the rest of the platform. No change to behaviour or response payloads; this is a visibility fix that helps us spot regressions faster.
2026-04-28
Fixed
- Rows in composite-primary-key tables now open correctly — clicking a row in a view backed by a table with a multi-column primary key (e.g. a
course_yeartable keyed oncourse_location_id+year) returned 400 instead of opening the row. The single-row API rejected the URL because the camelCase column keys it received from the frontend didn't match the snake_case names it was looking up internally. Lookup now accepts both conventions, so rows in composite-PK tables open normally for view, edit, and inline-edit flows.
Improved
- Softer confirmation when deleting metadata-only columns — deleting a relationship display column or a formula/computed column no longer shows the "DELETE"-typing confirmation with the harsh red warning. These deletes only remove the column metadata; the underlying foreign-key column data stays in the database, and formula columns never had stored data to begin with. The dialog now reflects that with a single OK/Cancel, an info-coloured (rather than destructive-red) message, and the line "No data will be deleted." Regular scalar columns (which DO drop the underlying database column) keep the stricter red confirmation.
2026-04-26
Improved
- More frequent off-site backups of platform metadata — orgs, users, schema definitions, billing state, and other platform metadata are now dumped to S3 every six hours (was: daily), retained for a week, with a documented restore procedure. Per the no-vendor-lock-in design, your own data still lives on your Postgres database — those databases are not part of these backups, and you remain responsible for backing them up. This change is about how quickly we can recover the platform side in the unlikely event of an issue with our infrastructure.
- 30-day log retention across the platform — internal log aggregation now keeps 30 days of history and survives container redeploys. Practical effect when you report an issue: we can investigate well beyond "what's in the container's memory right now," correlate across services, and trace what happened during recent deployments.
2026-04-24
Improved
- Faster production error detection — uncaught exceptions on both the frontend and backend are now forwarded to an error-tracking service in production, so regressions surface without waiting for users to report them. No change to behaviour; dev builds are unaffected.
- Gzip compression on API responses — Traefik now compresses application responses on the edge (SSE streams are excluded so real-time events still flow immediately), reducing payload sizes on large view reads and metadata endpoints.
Added
- One-command rollback for deployments — the deploy runner supports
run.sh rollback, which brings the previously-active blue/green color back up from existing images and swaps traffic over without rebuilding.
2026-04-20
Added
- View search in the toolbar — a new search box (right of the Help menu, under the hamburger menu on mobile) finds columns and cell values in the current view. Results open in a split dialog with two sections: Metadata for matching tables/columns (click to jump to the header and open its properties panel) and Row Data for matching cell values anywhere in the view (click to open the matched column's properties panel; if the row is already on screen, it's scrolled to and the cell pulses). Row cards offer Copy value and Filter by value quick actions — the filter is added as a temporary chip that only becomes permanent if you save the current preset. Keyboard: ↑/↓ to move between results, Enter to activate, Esc to close, and
Cmd/Ctrl+Kanywhere to focus the search input. Row-data search is server-side (case-insensitive substring across all columns), capped at 50 matches with a "refine your search" hint when more exist.
Improved
- Stronger regression coverage on MCP and relationship-picker code paths — internal test additions guard against bugs that previously slipped through: the MCP HTTP endpoint is now exercised end-to-end with real API keys (closing the gap where every test authenticated via JWT and missed MCP-key-specific failures), and the relationship picker's widget-type inference plus the OWNED inline editor's date input rendering are now covered by Playwright tests.
Removed
- Dead MCP JAX-RS auth filter — an unused
McpApiKeyAuthFilter(which never fired because MCP endpoints are Vert.x routes, not JAX-RS) has been deleted. Auth still runs as before viaMcpAccessGuard.requireAuth()invoked from each tool — no behavior change.
2026-04-19
Added
- Computed/Formula widget surfaces in the Add Column dropdown — the Formula widget type was previously hidden because it was misclassified as a relationship type. It now appears under a new "Computed" category alongside other widgets.
- Stable per-view column data key — every ViewColumn now carries a persisted
dataKeythat's the single source of truth for the row-JSON key, SQL alias, filter rules, presets, and frontend state. Two columns over the same path (e.g. a "Roles" chips column and a "Role Count" column) now get distinct keys (rolesName,rolesName_2) instead of overwriting each other in row data.
Improved
- Owned-mode inline editor pre-fills with the related row's data — opening a relationship cell in OWNED mode now fetches the linked entity's current values (e.g. the author's name/email/bio when editing a book's author inline) before the form renders, so saving sends a complete payload instead of empty NOT NULL fields.
- Owned-mode save updates every field in the nested form — the backend used to update only one column from the OWNED payload (and could pick the wrong one, e.g. setting the PK). It now matches each form field to the terminal entity's columns and updates them all in order, skipping the PK.
- Numeric widget consolidation — the redundant "Number" widget (functionally a duplicate of "Decimal" — both stored as NUMERIC) is removed. Existing scalar columns map cleanly: integer types → Integer, decimal/float types → Decimal.
Fixed
- Wrong PK in cell-edit URLs — opening a popover for a row with a non-trivial primary key sometimes called the API with
/0instead of the actual PK. The backend'sprimaryKeyColumnsresponse now derives PK aliases from the schema directly instead of looking them up in a column map that an M:N relationship column withdisplayField="id"could overwrite. - "Hidden FK column not found for relationship: …" 500 on relationship saves — when the relationship's internal name didn't match the FK column's camelCase alias (e.g. constraint-named relationships like
users_company_id_foreign), saving the relationship cell failed with a 500. The backend now matches the rel column to its hidden FK by the underlying snake_case column name. - Schema-sync OWNED mode crash on relationship-property resolution — the property resolver iterated a lazy collection on canonical view columns without fetching it, causing
LazyInitializationExceptionduring cell saves. Now uses theisRelationshipColumnflag, which doesn't trigger the lazy load.
2026-04-19 (earlier)
Added
- Relationship picker — drill into nested entities — you can now expand a nested relationship (e.g.
Enrolment ▸ personalPhysicalAddress) and pick a column from the leaf entity (Address.street) as the display field. Nested levels lazy-load on expand, so the picker stays fast on dense schemas and handles cyclic relationships safely. - Relationship picker — drill through association entities (rich junctions) — when a 1:N target is junction-shaped (a table that bridges two other entities, with or without extra columns), the picker now lets you keep walking through it to reach the other side. Example: from an Enrolment view, drill
invoiceEnrolments ▸ invoice ▸ invoice_numberto display invoice numbers per enrolment. Aggregate functions (Count, Chips, Sum, Avg, Min, Max, Concat) apply at any depth where a collection hop appears in the path. - Relationship picker — Concat aggregate — added a "Concatenated (joined string)" option to the display-mode dropdown for collection-valued relationships. Same SQL underpinnings as Chips (
string_agg/GROUP_CONCAT), but rendered as a single delimited string rather than chip widgets. - Relationship picker — Edit Mode in Add Column dialog — restores the dropdown that was previously available, letting you pick the cell's edit behavior (Not Editable / Reference / Owned / Association) at column-creation time instead of going to the properties panel afterwards.
Improved
- Picker — full-path collection detection — the aggregate-mode dropdown now appears whenever a 1:N or N:N hop sits anywhere in the relationship path (top-level OR nested), not just at the top level.
- Picker — Widget Type filtered by column type — the Widget Type dropdown now only shows widgets compatible with the selected column's database type (e.g. a YEAR column won't offer Email or Image). The list also accepts an "Inherit" option so the backend infers the default — matching whatever the source view uses.
- Picker — junction detection covers rich junctions — previously only pure junctions (PK = composite of FKs) were detected. Now also matches association entities (surrogate PK + composite UNIQUE on the FK pair, with extras), so the through-target M:1 surfaces under their 1:N back-pointer.
- Picker — alphabetical ordering — top-level rels in Existing Relationships and the entire Other Entities list are sorted alphabetically. Within each entity expansion: PK pinned at top, scalar columns alphabetized, then nested relationships alphabetized.
- Picker — cycle guard catches all path lineage — drilling like
Administration ▸ Enrolment ▸ Category ▸ Enrolmentis suppressed because the second Enrolment lands on the same row as the first. Previously only direct back-pointers to the view's source entity were filtered. - Picker — N:1 chip — added the missing N:1 chip alongside 1:1 / 1:N / N:N for visual consistency.
- Picker — clearer relationship labels — auto-generated relationship names (e.g.
fkC04d5114a1c904b3from hashed FK constraints) now derive a readable label from the underlying FK column name (personal_physical_address_id→ "Personal Physical Address"). Collection rels (1:N / N:N) use the relationship's own name rather than the back-pointer FK column.
Fixed
- Inline-edit popover prefills correctly for nested relationship columns — when toggling Owned mode on a relationship column that targets a nested field (e.g.
Enrolment ▸ Address ▸ address1), the form now uses the column's actual JSON key (fk…Address1) for prefill instead of the bare leaf name. Existing columns with the wrong saved value can be fixed by clearinginlineEditFieldsin the column properties. - API rejects FK-to-FK target columns — the backend now refuses to create a new relationship pointing at a column that is itself a foreign key. Previously the picker filtered these out client-side, but MCP/API callers could bypass the guard.
- Schema sync correctly classifies composite-PK FK columns as MANY_TO_ONE — previously, every column in a composite primary key was marked individually unique, so a foreign key whose column happened to be part of a composite PK (e.g.
book_authors_link.book_idinPRIMARY KEY (book_id, author_id)) got classified as ONE_TO_ONE. That suppressed the reverse 1:N back-pointer on the parent (e.g.books → bookAuthorsLinks), making rich-junction tables unreachable from the parent view's relationship picker. Re-syncing the workspace now creates the back-pointers correctly and the multi-hop drill-through works.
Improved
- Relationship picker — clearer existing-relationship tree — multiple foreign keys to the same table (e.g.
personalPhysicalAddressandcompanyPhysicalAddress, both →Address) are now distinguishable: each row shows the relationship name with the target entity as a chip. Foreign-key id columns are no longer offered as display values — drill into the relationship and pick a real field instead. - Relationship picker — Display Field dropdown removed for existing relationships — clicking a leaf column in the tree is the display field, so the redundant follow-up dropdown is gone. The dropdown still appears for new "Other Entities" relationships, where it controls the optional display column alongside the new FK.
- Relationship picker — cleaner "Other Entities" expansions — non-junction entities now show only their PK/unique columns (the valid FK targets). Nested-relationship drill-down is reserved for actual junction entities, where it backs the M2M through-path flow.
2026-04-17
Improved
- Relationship columns are editable by default — newly-added relationship columns now default to editable (dropdown picker that changes the foreign key) instead of read-only. The three edit modes — Reference (change FK), Owned (edit the related row inline), and Association (pick or create) — are exposed on the add-column flow. Read-only is still available as an explicit opt-in.
- Widget type is inferred for relationship columns — when you add a relationship column without specifying a widget, SchemaStack now picks a sensible default from the underlying database column's type (same inference used by schema sync). You can still override in the Properties Panel.
- AI/MCP
add_relationship_columntool — now acceptsreadonlyandrelationshipEditModeparameters, with documentation describing each mode.widgetTypeis optional — omitted, it's inferred.
Fixed
- MCP-originated relationship column delete — removing a relationship column through the MCP interface no longer throws a threading error mid-transaction; the delete completes cleanly.
- Widget Type selector is editable for relationship columns — the properties panel no longer blocks changing a relationship column's widget (previously disabled with "Foreign key columns cannot change type," which incorrectly applied to display widget changes).
- "Relationship data not loaded" message replaced with live loading state — the properties panel now shows a spinner while relationship metadata is loading, and only displays an error banner if the load actually fails.
Changed
- Add Column dialog split into two tabs — the dialog now has a Normal column tab and a Relationship column tab, cleanly separating scalar columns from relationship-backed columns. The Relationship widget option was removed from the Normal tab's widget dropdown. Existing relationship columns with
widgetType: RELATIONSHIPkeep working as before.
2026-04-16
Added
- Hide/unhide views — views can now be hidden from the tab bar and restored from the properties panel
- Hide/unhide columns — columns can now be hidden from the spreadsheet and restored from the properties panel
- MCP filter preset tools — AI assistants can now create, update, and delete filter presets via the MCP interface
Improved
- E2E test runner —
--grepflag now correctly filters to specific tests; fully isolated PostgreSQL container per run - E2E test helpers — API helper methods now use correct backend endpoints for column listing and updates
- View visibility SSE events — hiding/unhiding views broadcasts real-time updates to all connected users
- Auth resource cleanup — streamlined login endpoint code
Fixed
- View hidden state persistence — hidden views are now correctly saved and restored across sessions
2026-04-14
Added
- Login button on website — the brand website navbar now has a "Log In" button alongside the "Get Started Free" CTA
- Image column support in E2E seeds — books view now has sample cover images for testing
Improved
- Dashboard layout — organization description no longer overlaps the Settings button
- E2E test coverage — expanded from 212 to 1160+ tests across 24 spec files, covering all spread and admin app features
- E2E test stability — added cleanup hooks to mutation specs, fixed flaky filter/sort/M2M tests, improved login timeouts
- E2E infrastructure — isolated messaging and metadata database for tests (no longer shared with dev)
Fixed
- SERIAL column type handling —
SERIAL,SMALLSERIAL, andBIGSERIALPostgreSQL types are now correctly parsed as integers in the data layer
2026-04-12
Improved
- Formula column filters — formula columns that produce numeric results (e.g.,
price * quantity) now show the correct filter operators (greater than, less than, etc.) instead of text-only operators - Notification snackbars — redesigned with a neutral gray card, white inner content area tinted by type (green for success, red for error, amber for warning), with dark mode support
- Schema drift dialog — clean centered layout when schema is in sync, replacing the old message box
- CSV import column matching — improved column mapping dialog with better auto-detection
- Login dialog accessibility — fixed Angular content projection warning for button icons
Fixed
- Dark mode consistency — forced dark mode now matches browser-detected dark mode exactly across all components, chips, row colors, and Material styles
- Filter evaluator type detection — numeric formula results are now correctly compared as numbers in client-side filter evaluation
2026-04-11
Added
- Conditional Row Styles — color and format rows based on filter conditions. Create rules like "status = active → green background" with support for background colors, bold, italic, and strikethrough. Rules are priority-ordered (first match wins) and saved as part of filter presets, so shared presets include their visual formatting
- Live preview in row styles dialog — conditional formatting rules now apply to the data table in real-time while editing, so you can see the effect before saving
Improved
- Dark mode for conditional row colors — the 8 preset row background colors now properly adapt to dark mode in both "Match Browser" and forced dark theme
- Readonly cell tinting — readonly cells on conditional-colored rows now darken the row color instead of always showing a blue tint
Fixed
- Column width stability on infinite scroll — column widths no longer jump when loading additional rows via infinite scroll
- Drift detection false positives for unique indexes — columns marked as unique no longer trigger spurious "index added" drift warnings
- Drift detection false positives for timestamp defaults — auto-generated
now()defaults on timestamp columns no longer reported as drift
2026-04-10
Added
- CSV Import — import CSV files into any view with a Sequel Ace-inspired field mapping dialog. Supports auto-mapping by column name, record browsing to verify mappings, and saved mapping presets for repeated imports
- Bulk insert endpoint — new
POST /api/data/bulk/{viewUuid}/insertfor inserting up to 1000 rows per request, with per-row error reporting - Import mapping presets — save and load CSV-to-view column mapping configurations. Supports PRIVATE and SHARED visibility, same pattern as filter presets
Improved
- Tab bar scroll — active tab now scrolls into view on page load when there are many tabs
- Tab chevron sizing — dropdown chevron properly contributes to tab width instead of overlapping
- Properties panel elevation — panel now has a subtle box-shadow for better visual separation
- Properties panel drag handle — thicker green bar on hover, only opens panel on drag (not click), drag-to-close supported
- Relationship picker — circular references (current entity) hidden in both existing and available entity trees; empty junction table children no longer shown
- Widget type for relationships — FK columns now show "Relationship" in the widget type dropdown instead of empty
- Insert column dialog — title split into "Insert Column" heading with position as subtitle
- Preset dialog spacing — reduced vertical gap between search input and filter/sort chips
Fixed
- Hardcoded PK column assumptions eliminated — all relationship query builders now use actual PK column names from metadata instead of assuming "id". Missing metadata throws clear errors instead of silently producing wrong SQL
- Target entity metadata cached on relationships —
targetTableNameandtargetPkColumnstored during sync/creation, eliminating runtime DB lookups in query builder - Display mode switching — changing between Count/Values/Aggregate now sends a single API call instead of two, fixing stale
displayFieldbeing sent - Guest token access recording — fixed session lifecycle error in
GuestTokenAuthFilterby chainingrecordAccessinto the reactive pipeline - Dashboard broadcast stability — set read-only flush mode to prevent
StaleObjectStateExceptionduring concurrent entity deletions
2026-04-09
Added
- OneToMany relationship columns — display child record counts or aggregated values (chips) from related tables. Supports rollup functions: Count, Sum, Average, Min, Max
- Relationship type labels — discovery tree shows cardinality badges (1:1, 1:N, N:N) for each relationship
- Relationship search filter — filter entities by name in the relationship picker for large schemas
- Clear FK value — new "Clear selection" option in the relationship cell editor to set a foreign key to NULL
- Display mode switching — toggle between Count and Values (chips) for M2M and OneToMany columns after creation
- Rollup aggregate functions — Sum, Average, Min, Max aggregations for OneToMany columns
Improved
- M2M column properties — simplified panel hides irrelevant schema settings and referential actions
- M2M real-time sync — association toggles now broadcast to other users via SSE
- UUID support in M2M editor — multi-select editor handles both numeric and UUID primary keys
- Junction entity detection — relationship picker auto-detects junction tables and hides PK columns
- Column position management — all column creation paths (regular, formula, relationship, M2M) now use the position manager for correct insertion ordering
Fixed
- Schema sync cascade failure — sync now uses isolated sessions per view, preventing one view's failure from cascading to all others (25P02 fix)
- Schema drift false positives — synthetic M2M columns excluded from drift detection
- M2M column deletion — proper Hibernate cascade cleanup prevents OptimisticLockException
- Stale column backfill — sync no longer tries to create ViewColumns for columns deleted in the same transaction
- Primary key detection — relationship display columns no longer collide with PK columns in query metadata, fixing incorrect rowId resolution for all cell editors
- Relationship options on MySQL — FK value lookup now uses vendor-aware SQL, fixing the "selected" indicator in relationship dropdowns
- M2M toggle returns full row — association changes now return the complete updated row, ensuring computed columns refresh correctly
2026-04-08
Added
- Many-to-many relationships — link records across entities with a new "Multiple links" option in the relationship picker. Creates a join table automatically — no manual schema setup needed
- M2M chips display — many-to-many columns show linked records as compact colored chips in the spreadsheet grid
- M2M association endpoints — new API endpoints for managing many-to-many associations (add/remove links, fetch options with search and pagination)
- AI usage counter — daily AI message usage shown in the chat title bar (e.g. 2/3), resets daily per member
- Chat history — conversation persists in the browser per workspace, survives page reloads. Use
/clearto reset - Chat help button —
?icon in chat input shows available commands (/clear,/help)
Improved
Column position SSE — moving a column now broadcasts all affected positions to other users, fixing visual ordering glitches in real-time
Schema Advisor rate limits — daily AI message limits per member by plan: Free (3/day), Pro (20/day), Enterprise (unlimited). When limits are reached, users are guided to connect their own AI client via MCP for unlimited access
Workspace status banners — consistent purple Design mode color across admin and spread apps, with dark mode support. Status changes from admin now update the spread app banner in real-time
Mobile banner layout — status and connection banners now have proper padding and icon sizing on narrow screens
Floating panels — shared drag handle and close button styles between Activity and Chat panels, uppercase titles matching table headers
Fixed
- Workspace status SSE — changing workspace status in admin now immediately updates the banner in the spread app (was filtered as own-event)
- Column update persistence — relationship edit mode and other display properties now correctly saved via schema detection path
2026-04-07
Added
- Schema Advisor — AI-powered schema assistant built into the spreadsheet interface. Inspects your schema, suggests improvements, creates tables/columns/indexes/constraints, and manages relationship columns through natural conversation. Only schema metadata is sent to the AI — your actual data stays private. Learn more
- Column search API — fuzzy search for columns by name or display name across all views in a workspace, with trigram similarity matching for typo tolerance
- "Start with sample data" workspace option — new template-based workspace creation that provisions a managed database pre-loaded with the Acme Store demo dataset (categories, products, customers, orders). Available from the create workspace wizard
- Database indexes documentation — new public docs page covering index creation, listing, deletion, schema import, and best practices
- Relationship lookup columns via MCP — add columns from related entities (including multi-hop relationships) directly through AI assistants
Improved
- Workspace detail header — "Open Workspace" replaced with a clean "Open in SchemaStack" link; Settings button uses gradient styling aligned with card padding
- Roadmap updates — moved database indexes, computed columns, migration impact prediction, MCP server, and Zapier integration from roadmap to "Available Now"
- Floating panels — activity and chat panels share a consistent draggable window design with title bars
- Properties panel — dragging the resize border now opens the panel if it was collapsed
- Email verification — first input auto-focuses for immediate paste support
Fixed
- MCP column creation — fixed
BlockingOperationNotAllowedExceptionwhen adding relationship columns via MCP (JWT identity resolution blocked on IO thread) - Column update SSE — UI-only column changes (readonly, edit mode, position, display name) now broadcast real-time to other users
- Column update persistence — relationship edit mode, sortable, apiVisible, and other display properties now correctly saved via the schema detection update path
- Relationship column positions — new relationship/lookup columns now get a valid position instead of null
- Workspace slug reuse — deleted workspaces and organisations free up their slug for reuse
2026-04-06
Improved
- Zapier integration — new row and updated row triggers now automatically detect common timestamp column names so triggers work regardless of your column naming convention. Supported names:
- New Row:
created_at,createdAt,created,inserted_at,insertedAt,inserted,insert_at,insertAt,date_created,dateCreated,creation_date,creationDate,created_date,createdDate,added_at,addedAt,added - Updated Row:
updated_at,updatedAt,updated,modified_at,modifiedAt,modified,update_at,updateAt,date_modified,dateModified,date_updated,dateUpdated,modified_date,modifiedDate,updated_date,updatedDate,last_modified,lastModified,last_updated,lastUpdated,changed_at,changedAt
- New Row:
- Zapier "Find Row" search — search field is now a dropdown populated from your table's columns instead of freeform text
- Zapier deduplication — updated row trigger no longer fires on newly inserted rows, and correctly re-triggers on each update
- Bulk export downloads — export files are now stored in S3, fixing download failures when the processor and API run on separate containers
- Export download reliability — download links no longer disappear on transient errors; only expired exports are removed from the toolbar
- Timestamp behavior — columns with "On update" auto-set now get an initial timestamp on insert (previously NULL via both the Data Platform and the Workspace API)
Fixed
- Migration tracking — fixed stale migration locks between services causing "migration already in progress" errors
- Timestamp behavior migrations — dry-run no longer corrupts auto-generation state, preventing subsequent migrations from being silently skipped
- Add row form — columns with auto-set timestamp behavior are now hidden from the form (they're auto-generated)
- Workspace API filters — invalid filter values (e.g., text in a UUID field) now return empty results instead of a 500 error
- Workspace API stability — fixed crash when tables have foreign key columns that share a primary key (e.g., shared-PK inheritance patterns), and tables without a primary key (e.g., Liquibase changelog tables) no longer break the API for the entire workspace
- Status page reliability — health checks no longer report false "down" status when services are deployed individually on different blue/green colors
2026-04-05
Added
- Column rename — rename database columns directly from the column properties panel in Schema Settings
2026-04-04
Added
- Outbound webhooks — configure webhook endpoints on any view and send selected rows to external services with a single click. Includes HMAC-SHA256 payload signing, automatic retries with exponential backoff, and a delivery log for tracking each request
- "Send to webhook" bulk action — select rows in the Data Platform, choose "Send to webhook" from the bulk action menu, and pick which configured endpoint to send to
- Zapier integration — new native Zapier app lets you automate workflows: trigger on new/updated rows, create or update rows from other apps, and search for rows by field value
- Webhook delivery log — view the status, response code, duration, and retry count for each webhook delivery attempt directly in the view properties panel
- Row-level security (RLS) — per-view policies that filter data based on JWT claims, so external users only see and modify their own rows (e.g.,
customer_id = {jwt.sub}) - External identity provider support — workspace owners can now configure an external OIDC provider (Auth0, Clerk, Firebase) so end-users can authenticate with their own accounts and call the workspace API directly without a SchemaStack account
- Passwordless database authentication — workspaces can now connect to databases using IAM, peer, or certificate-based auth without requiring a password
- Processor test suite — comprehensive integration tests for column creation types, column drop/default, foreign key operations, index operations, table creation, and error handling
Improved
- Schema sync — improved composite foreign key extraction, drift detection, and schema hashing for more reliable sync
- Schema repair — diagnose now detects partially missing view columns (not just fully empty views) and repair backfills them
- Repair display — repair report now shows the exact missing column names instead of just a count
- Column properties panel — improved relationship picker and column schema options UI
- Migration warning — clearer migration impact warnings in the column properties panel
Fixed
- Missing view columns after failed task completion — when an internal message was lost during column creation, the view column was never created. Schema sync and repair now detect and fix these orphaned column metadata rows
- Member limit — settings page now shows the correct member limit from your subscription tier instead of a hardcoded value
- Query performance — fixed Hibernate in-memory pagination (HHH90003004) on organisation queries; memberships are no longer loaded entirely into memory
Removed
- SQL Server vendor option — removed from workspace creation (PostgreSQL and MySQL only)
- Push notifications — removed non-functional push notification settings (email preferences remain)
2026-04-03
Added
- OAuth2 Authorization Code + PKCE — workspace API now supports OAuth2 token-based access for SPAs and mobile apps, alongside existing API keys
- OAuth2 client management — new "OAuth2" tab in workspace settings to register and manage OAuth2 clients with redirect URIs and scopes
- OAuth2 consent page — when a third-party app requests access, users see a consent screen showing the app name, workspace, and requested permissions before approving
- API key expiration — API keys can now be created with an optional expiry date
Improved
- CORS handling — workspace API now allows all origins for token-authenticated requests, following industry standard (Stripe, Supabase); CORS configuration removed from settings since Bearer tokens provide the security, not origin restrictions
- Multiple foreign keys to same table — workspace API now correctly handles tables with multiple FKs pointing to the same target table
2026-04-02
Added
- Composite foreign key support — full stack support for multi-column foreign keys: sync extracts them, DDL generates
FOREIGN KEY (a,b) REFERENCES target(x,y), ByteBuddy generates@JoinColumnsannotations - Composite PK relationship picker — when a target table has a composite primary key, the picker shows checkboxes with a "Select all PK columns" shortcut for multi-column FK creation
- Composite PK bulk operations — bulk delete, update, and export now support tables with composite primary keys using
(col1,col2) IN ((?,?),(?,?))syntax
Improved
- Input validation — all REST endpoints now validate request bodies; invalid input returns clear 400 errors instead of server errors
- Security headers — added X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, and Referrer-Policy headers on all responses
- Password reset security — response is now identical whether the email exists or not, preventing account enumeration
- CORS security — workspace API CORS headers are now set via a dedicated servlet filter
- Error messages — internal error details no longer leak in API responses; 500 errors return generic messages
- JDBC timeouts — all database connections and queries now have timeouts to prevent indefinite hangs
- File upload protection — oversized uploads are rejected before reading into memory
Fixed
- Bulk operations with composite PKs — scalar IDs on composite PK tables correctly use the first PK column instead of failing with a cast error
- Column creation positioning — inserting a column at a specific position now correctly shifts existing columns using the position manager
Removed
- Deprecated relationship endpoint — removed
POST /api/views/{uuid}/relationship-columnsand MCPadd_related_columntool; usePOST /api/columnswithrelationshipPathinstead - Unused dependencies — removed Apache Camel (unused), dead code, deprecated consumer class, unused repository methods
2026-04-01
Added
- Database view support — database views (CREATE VIEW) are now imported during schema sync as read-only views; all columns are marked non-editable
- Generated column support — columns defined as
GENERATED ALWAYS AS (...)are automatically detected and marked as read-only, preventing insert/update errors - Column comments as display names — if your database columns have comments (
COMMENT ON COLUMN ...), they're used as the default display name instead of converting from snake_case - PostgreSQL ENUM support — enum types are detected with their allowed values extracted from the database
- PostgreSQL array type support — array columns (
TEXT[],INTEGER[]) are imported and displayed as text - Database compatibility guide — new documentation page listing all supported and unsupported database features for PostgreSQL and MySQL
Improved
- Schema sync reliability — new per-view transaction architecture ensures one table failing doesn't break the entire sync; each table's success or failure is independent
- FK cascade rule syncing — foreign key cascade actions (ON DELETE CASCADE, SET NULL, etc.) are now properly synced and kept up to date when changed in the database
- Widget type detection — expanded to cover 30+ database types including JSONB, UUID, INTERVAL, MONEY, TIMESTAMPTZ, DATETIME, MEDIUMTEXT, and more
- Column positioning — creating a column at a specific position now correctly shifts existing columns using the position manager
- Auto-generation type syncing — changes to auto-generation (e.g., switching from SERIAL to IDENTITY) are now detected and synced
- Native image build speed — removed unused Apache Camel dependency and excluded large SDK JARs from resource scanning
- Native image compatibility — comprehensive reflection config for all DTOs ensures correct serialization in native builds
Fixed
- ViewColumn generation on sync — views imported by the old sync that were missing ViewColumns now get them automatically on re-sync
- Relationship columns on fresh import — FK columns now correctly create both a hidden FK ViewColumn and a visible relationship ViewColumn with the RELATIONSHIP widget
- Column ordering — ViewColumn positions now follow database column order (JDBC ordinal position) instead of arbitrary ID order
- Password reset security — response is now identical whether or not the email exists, preventing user enumeration
Removed
- Old sync engine — removed the monolithic SchemaImportService (~2,600 lines) and SchemaSyncService wrapper, replaced by the new per-view sync architecture
- Deprecated consumer — removed unused TaskCompletionConsumer (functionality merged into TaskCompletionBusinessLogicConsumer)
- Dead code cleanup — removed unused repository methods, debug print statements, and unused messaging DTOs
2026-03-31
Added
- Per-view sync architecture — schema sync now uses independent transactions per table; if one table fails, others still sync successfully
- Relationship widget type — unified column creation with inline relationship configuration in the properties panel
- Formula/computed columns — virtual columns evaluated at query time using SQL expressions, created via
POST /api/columnswith aformulafield - Unified column creation API — relationship columns and regular columns now use the same
POST /api/columnsendpoint
Improved
- Relationship picker — embedded inline in the column properties panel with source column auto-hidden when active
- Column creation dialog — enhanced with widget type selector, reference picker, and formula editor components
- Migration warning component — new UI component showing impact warnings before schema changes
Fixed
- FK cascade action syncing — foreign key ON DELETE/ON UPDATE rules are now properly extracted from the database and synced to column metadata
- ViewColumn generation — fresh imports now correctly generate ViewColumns for all columns
- Relationship ViewColumns — fresh import creates both hidden FK and visible relationship ViewColumns with correct widget type
2026-03-28
Fixed
- Schema sync column changes — syncing a workspace after columns are added or removed in the database now works correctly; previously this could cause internal errors due to Hibernate cascade conflicts
- Schema drift false positives — composite unique constraints (e.g. multi-column unique indexes) no longer incorrectly report individual columns as having unique constraint changes
- Feedback screenshot upload — fixed threading error that prevented screenshot uploads; upload now runs on a worker thread pool
- Feedback dialog reset on error — when a screenshot upload fails, the drag area now resets instead of showing a broken preview
Improved
- Feedback screenshot storage — screenshot metadata is now stored as a single JSON field (matching the image widget format) instead of two separate columns
- Feedback screenshot preview — the dialog now shows a server-confirmed preview image instead of a local base64 preview, ensuring the upload actually succeeded
- Avatar upload buttons — "Upload Avatar" and "Use This Image" buttons now use flat green style consistent with the rest of the app
2026-03-27
Fixed
- Warning notifications — warning snackbar no longer appears transparent over the toolbar
- Schema drift detection — composite unique indexes no longer cause false-positive drift; FK cascade actions are now synced correctly
- Feedback screenshots — large screenshots are now compressed client-side before upload, preventing timeouts
Added
- Foreign key cascade rules — configure ON DELETE and ON UPDATE actions (Cascade, Set Null, Restrict, Set Default) per foreign key column in the column properties panel under "Referential Actions"
- Default preset per view — set a personal default preset that auto-loads when opening a view (pin icon in preset dialog), each user has their own independent default
- Schema import preserves cascade rules — importing an existing database now detects and stores both ON DELETE and ON UPDATE rules from FK constraints
2026-03-26
Added
- Schema repair tool — new Repair tab in workspace settings diagnoses and fixes metadata inconsistencies (orphaned entities, missing view columns)
- Add Column from empty view — views with no columns now show an "Add Column" button, and the view tab menu includes an "Add Column" option
- API Docs & Sandbox in Help menu — the spread app Help menu now links to API documentation and Swagger sandbox
- Feedback screenshot drag & drop — the feedback form now supports dragging and dropping images in addition to clicking to upload
- Composite primary keys — the workspace API now supports entities with composite primary keys (comma-separated in URL path)
Improved
- Timestamp behavior changes — changing a column's auto-set timestamp behavior now shows the unified orange migration warning box instead of a separate inline banner
- Real-time connection stability — switching between view tabs no longer shows a false "Connection was interrupted" warning
- Migration conflict handling — when a column update conflicts with an in-progress migration, a "Force Cancel Migration" action lets you cancel and retry
- Workspace API validation — auto-generated columns (timestamps, UUIDs) and columns with default values are no longer required in POST requests
- Workspace API type support — added support for 25+ SQL types (SMALLINT, SERIAL, JSONB, TIMESTAMPTZ, BYTEA, etc.) across entity generation, validation, filtering, and OpenAPI docs
- OpenAPI documentation — Swagger docs now show nullable indicators, readOnly markers, default values, and correct types for UUID/composite primary keys
Fixed
- UUID primary key CRUD — creating, reading, updating, and deleting records in tables with UUID primary keys now works correctly
- Auto-generated timestamp columns — columns with
DEFAULT CURRENT_TIMESTAMPare no longer included in INSERT statements (lets the database default apply) - Null value updates — setting a nullable field to null via PUT request now works instead of being silently ignored
- Bulk operations with UUID PKs — bulk update and delete now correctly handle UUID and composite primary keys
- Filtering on UUID/date columns — filtering by UUID, DATE, and TIMESTAMP columns no longer causes type mismatch errors
- Dark mode badge colors — validation rules, filter badges, and other mint/blue badges now display with subdued colors in dark mode instead of bright green
- Dark mode consistency — fixed sidebar going dark while main content stayed light when OS dark mode is active
- Add Row with UUID primary key — creating records in views with UUID primary keys no longer fails with "Field 'uuid' is required"
- Schema drift on UUID PK views — creating a view with a UUID primary key no longer triggers false "Column type changed" drift detection
- Schema sync data loss — syncing a workspace no longer deletes view columns from other workspaces (critical fix)
- Double data load on column add — adding a column no longer triggers two redundant data queries causing a table flicker
- Workspace Overview link — the avatar menu in the spread app now correctly links to the current workspace instead of the organization slug
- Database settings form validation — credential fields now properly update validation state when pre-filled from saved settings
- Test cleanup — added missing
workspace_activity_logtable to test cleanup, fixing cascade failures in metadata tests
Security
- Authorization audit — added view-level permission checks to relationship endpoints, bulk action endpoints, and removed unused unscoped query methods
2026-03-25
Improved
- View deletion UX — deleting a view now instantly removes the tab and navigates away, with a confirmation dialog instead of a browser prompt
- View creation UX — creating a view shows a pulsing tab with spinner while the backend processes, and a loading indicator in the main content area
- MCP key badges — changing the workspace MCP access level now immediately updates the effective access badges on all keys
- SSL certificate validation — the "Test Connection" button now includes uploaded SSL certificates in the test, and the CA certificate field is required when using VERIFY_CA or VERIFY_FULL mode
- SSE reconnect awareness — a subtle banner appears after a real-time connection interruption, offering a one-click refresh to sync any missed changes
Fixed
- UUID primary key on view creation — creating a view with a UUID primary key type now correctly generates the primary key column without needing an extra flag
- Read-only table after view creation — views created with a primary key are no longer incorrectly marked as read-only
- Constraint SSE events — constraint changes (add, update, toggle, delete) now broadcast on the view stream and include column name and constraint type for richer activity messages
- Richer real-time events — column created events now include widget type and database name; view member/guest events include the view name; preset events include the preset name; API key revoked events include the key name
- Dark mode — the "Your Canvas Awaits" empty state and delete confirmation dialog warning text now display correctly in dark mode
- Real-time event data — SSE events now use flat data access consistently, fixing "undefined" values in activity messages and key creation notifications
2026-03-24
Added
- Database indexes — create, list, and delete single or multi-column indexes (including unique indexes) on views via the new Indexes API
- FK cascade rules — foreign key columns now support ON DELETE actions (CASCADE, SET NULL, RESTRICT, SET DEFAULT) that are stored in metadata and applied during schema generation
- Index import — importing an existing database schema now detects and preserves database indexes (composite and non-unique) as metadata
Improved
- Unique index DDL generation — unique indexes are now generated as
CREATE UNIQUE INDEXinstead of regular indexes in the schema processor - Schema import accuracy — composite unique indexes are no longer incorrectly flagged as single-column unique constraints during import
2026-03-23
Added
- Views usage card on dashboard — the organisation dashboard now shows a Views stat card with usage bar, matching the existing Workspaces, Members, and Storage cards
Improved
- Dashboard handles unlimited plan limits — stat cards now hide the usage bar when a plan limit is unlimited (null) instead of showing "0 of 0"
Fixed
- "undefined B" storage display — the Storage stat card on the Plan & Billing page no longer shows "undefined B" when storage is zero or not reported
- Properties panel dark mode on "match browser" theme — the column properties panel in the spread app now correctly uses dark background when the browser is set to dark mode (previously only worked with explicit dark theme toggle)
- Plan & Billing page crash for new orgs — fixed "can't access property 'length' of undefined" error when the workspace list is not returned by the API
2026-03-22
Added
- Demo video scripts — automated Playwright-based screen recordings for admin and spread app demos, with narration text files and reset scripts for reproducible recordings
- YouTube demo video embedded on website — the landing page hero section now shows the product demo video instead of a placeholder, and "Watch Demo" buttons scroll to it
Improved
- Create workspace now includes Connection Security settings — SSL/TLS mode, connection timeout, and certificate upload options are now available when configuring a database during workspace creation, matching the existing workspace settings form
- Workspace links fixed in production — "Open Workspace" and "Open in SchemaStack" links on the workspace overview page now correctly route to the spread app
- Pro plan pricing reduced — Pro plan price lowered from $29/month to $19/month
- Dark mode input borders — cell edit popover and dialog form fields now use subtle border styling instead of bright primary-color outlines in dark mode
- Loading bar no longer shifts login form — the progress bar on the login page now overlays the page (matching the authenticated layout) instead of pushing content down
- Bulk action bar styling — column select dropdown is now compact with proper font weight
- Cell edit autofocus — opening the cell edit popover now automatically focuses the first input field
- Boolean bulk edit default — boolean fields now default to "false" in bulk edit mode instead of null, preventing validation errors
Fixed
- Validation error messages now show details — API validation errors (e.g. "Description cannot contain < or > characters") now display the specific field error instead of just "Validation failed"
- Registration no longer briefly flashes a red error box on success before navigating to the verification page
- DECIMAL widget icon now renders correctly (was using non-existent Material icon name)
- Schema drift dialog expansion panels no longer show dark borders in light mode
- Organisation description with newlines — saving a description containing line breaks no longer triggers a false "cannot contain < or >" validation error. Newlines are now automatically converted to spaces
- Relationship columns positioned correctly on sync — when a new workspace is synced, relationship display columns (e.g., "Category") now appear in the same position as the foreign key column in the database schema, instead of being appended at the end
- Database cascade cleanup — fixed missing ON DELETE CASCADE constraints across the schema, preventing orphaned rows after workspace or view deletion. Reset scripts are now simpler and more reliable
2026-03-20
Improved
- Filter preset visibility simplified — the three visibility options (Private, Team, Public) have been reduced to two: Private (only you) and Shared (anyone with access to the view). The "Public" option was redundant since view access already requires workspace membership. Existing Team and Public presets are automatically migrated to Shared
2026-03-18
Added
- Composite primary key support — tables with multi-column primary keys now support full CRUD operations (view, edit, insert, delete). Composite key values are passed as JSON objects in both the REST API and SSE events
- Read-only mode for tables without primary keys — tables that have no primary key can now be viewed and exported, but write operations (edit, insert, delete) return a clear error explaining that a primary key is required
- Export file retention — bulk exports now include an expiration timestamp (default 24 hours). The download link and SSE event show when the file expires so you know how long it remains available
- Scroll-to-warning button — when a pending schema migration warning is scrolled out of view in the properties panel, a floating orange pill button appears to quickly jump back to it
Improved
- Admin dark mode background — the admin app now uses a subtle dark blue gradient (matching Tailwind's gray-950/gray-900) across all pages in dark mode, replacing the flat background
- Shared login dark mode styles — both spread and admin login pages now share the same dark mode card styling (emerald gradient tint, green border, green-tinted shadow, dark input backgrounds) via a shared SCSS mixin
- SSE reconnection banner no longer flashes on page load — the "Reconnecting to real-time updates" banner is now suppressed during the initial SSE connection and only appears on actual reconnection attempts
Fixed
- Filtering on date and timestamp columns now works correctly — previously returned a type coercion error when using date filters like
shipped_at > 2025-11-01 - Bulk operations (delete, update, export) now work correctly on tables with UUID primary keys — previously failed with a type conversion error
- Bulk delete/update that completely fails (e.g. due to a foreign key constraint) now correctly reports as failed instead of succeeded. The error response includes per-row details explaining why each row could not be processed
- Spread login card width now matches admin at 448px
- "Forgot password?" link in spread login now renders at 14px as intended
2026-03-17
Added
- Migration impact preview with SQL highlighting — when a schema change requires a database migration, the properties panel now shows the exact SQL operations that will be executed, with syntax-highlighted queries, impact level classification, and a list of affected foreign key references with read/write impact badges
- Real-time migration progress — during long-running schema migrations, the system now reports real-time progress via SSE (
view.column.schema.progressevents). For PostgreSQL 12+, progress is derived frompg_stat_progress_alter_tablewith actual rows-processed counts. For MySQL and older PostgreSQL, time-based progress estimation is used instead. Progress includes phase info (e.g. "Rewriting table", "Building index"), percentage complete, and elapsed time - API protection during migrations — the REST API and MCP tools now return
503 Service Unavailablewith aRetry-Afterheader when a blocking schema migration is in progress, instead of hanging on database locks. The error response includesblocksReads,blocksWrites, andretryAfterSecondsso clients know exactly what's blocked and when to retry - Data-driven duration estimates — migration duration predictions are now refined over time using historical data from your workspace. After enough migrations, the system replaces static assumptions with actual measured ms/row rates specific to your database hardware
- Row count estimation for migrations — migration impact predictions now use real approximate row counts from the workspace database (
pg_stat_user_tablesfor PostgreSQL,information_schema.TABLESfor MySQL), making duration estimates far more accurate for large tables - Migration duration tracking — the system now records actual migration durations and compares them to estimates. Over time, this historical data refines future predictions — your workspace's actual hardware performance replaces static assumptions
- MySQL storage engine detection — impact analysis now detects whether a MySQL table uses InnoDB, MyISAM, or another engine, and adjusts the impact classification accordingly. Non-InnoDB tables are classified as fully blocking since they lack Online DDL
2026-03-16
Added
- Migration impact prediction — when a schema change requires a database migration (column type change, adding NOT NULL, etc.), the system now predicts the impact: whether it will be instant, brief, or blocking, whether reads and/or writes are affected, and the estimated duration. This is vendor-aware — the same operation can have different impacts on PostgreSQL vs MySQL (InnoDB vs MyISAM). Impact data is included in the 202 API response and in the
view.column.schema.changingSSE event
Improved
- SSE event type naming has been reorganised — the event prefix now directly determines which stream carries it:
view.*events go to the view stream only,workspace.*events go to the workspace stream, andorganization.*/dashboard.*events go to the org stream. Data events are now prefixedview.data.*and view lifecycle events are nowworkspace.view.* - Migration prediction now correctly detects all database type changes (e.g. VARCHAR → TEXT) — previously, types in the same family were incorrectly skipped. Only true aliases (e.g. VARCHAR ↔ CHARACTER VARYING) now bypass migration
- Faster initial load — non-core UI components (properties panel, activity feed, filter panel, bulk action bar) are now lazy-loaded, reducing the initial bundle size
- Migration info popover is now readable in both light and dark mode — text colors are hardcoded to a dark theme for consistent contrast
- Migration rule descriptions are now more informative — each rule shows context-aware detail text (e.g. "Not applicable for VARCHAR type" instead of generic "No change")
- Widget type changes now correctly trigger migration detection — changing a widget from STRING to NUMBER (or any change that implies a different DB type) is properly routed through schema change detection instead of being silently applied as a metadata-only update
- Column migration prediction is now fully accurate for all PostgreSQL and MySQL type variants — the backend correctly distinguishes between true type aliases (no migration) and different types (migration required)
- All 16 widget types (STRING, TEXT, EMAIL, URL, PHONE, NUMBER, INTEGER, DECIMAL, BOOLEAN, DATE, DATETIME, SELECT, MULTI_SELECT, FILE, IMAGE, UUID) now have complete widget configurations — creating columns with any widget type works without errors
- Schema processor now handles cross-vendor type mapping for all common types — MySQL-specific types (MEDIUMTEXT, TINYINT, ENUM, SET, YEAR) are correctly translated when targeting PostgreSQL, and PostgreSQL-specific types (INTERVAL, INET, BYTEA, JSONB, SERIAL) are translated when targeting MySQL
Fixed
- Fixed
currentDbTypereturning widget type names (URL, EMAIL, PHONE) instead of actual database types (VARCHAR) in the column properties response - Fixed widget-type-only changes (without advanced options) bypassing schema change detection entirely — VARCHAR→INTEGER type changes were applied without generating the required database migration
- Fixed NUMBER widget mapping to INTEGER instead of NUMERIC — now correctly maps to NUMERIC to match the widget's default precision/scale settings
- Fixed FILE and IMAGE widget DB type mapping — now correctly maps to TEXT (matching their widget configuration) instead of VARCHAR
2026-03-15
Added
- Image columns now support a URL mode (
imageMode: "url"in widget options) — set this when your column contains plain image URLs instead of uploaded files. Thumbnails are generated on first view and cached weekly. Failed URLs are remembered to avoid repeated fetch attempts - Signed thumbnail URLs for secure access without requiring authentication headers on image tags
- Organisation description is now included in the selected organisation response, so dashboard and settings pages can display it without an extra API call
Improved
- SSE events now include
originClientIdanduserIdacross all event types — enables reliable echo prevention so your own changes don't trigger duplicate UI updates - Organisation settings SSE events now send only the changed fields instead of the full organisation object, and use the organisation slug as entity identifier
2026-03-14
Improved
- Organisation member role changes and removals now broadcast real-time SSE events — other admin users see member updates instantly without refreshing
- All SSE streams now enforce membership checks at connection time — workspace stream requires workspace membership (or org admin), organisation stream requires org membership, and task completions require authentication
- SSE event type naming is now consistent — all column events use the
metadata.view.column.*prefix for a uniform hierarchy - SSE event payloads now follow a uniform structure — create events include the new entity under
data.entity, update events include only changed fields underdata.changes, and delete events include identifiers only - Workspace slug can now be updated via the workspace settings endpoint
- Workspace status change responses now include the
previousStatusfield - View permissions dialog now updates in real time — member and guest changes by other users appear instantly via SSE
- When an admin syncs the database schema, other users viewing the spreadsheet see a warning banner prompting them to refresh for the latest columns and data
- Other admin clients now silently update schema status metadata (view count, last checked, last synced) without showing disruptive alerts
Added
- View-level SSE stream (
/sse/view/{orgSlug}/{workspaceSlug}/{viewUuid}/stream) — only users with a view open receive view-scoped events like cell edits, column changes, and bulk operations, with permission checked at connection time - Real-time notification when view access flags (addable, editable, exportable) change — other users see updates instantly
- Real-time notification when a schema sync or drift check completes — other users on the workspace page see updated sync status, drift results, and last-checked timestamps without refreshing
- Real-time notification when organisation subscription plan or status changes (e.g. upgrade, cancellation)
Fixed
- Row insertion now correctly checks the view's "addable" flag — previously it checked "editable" instead, allowing row inserts on views that had editing enabled but adding disabled
- Workspace database config updates now persist correctly — previously the update response showed the right values but stale data could appear in subsequent reads
- Login and signup pages now respect your saved dark mode preference
- New workspaces now appear at the top of the dashboard list instead of the bottom
2026-03-13
Added
- Real-time dashboard updates — usage stats and recent activity now push automatically via SSE when workspaces, views, or members change, so the dashboard always shows current data without refreshing
- Dedicated database config endpoints (
GET/PUT/DELETE /api/workspaces/{uuid}/database-config) — manage database connection settings independently from the workspace, consistent with storage, API, and MCP config endpoints - Standalone database connection test (
POST /api/database-config/test) — validate database credentials before creating a workspace - Workspace list and detail responses now include a
databaseNamefield for quick reference without loading full config
Fixed
- Filter presets now apply their saved filters and sort order when loading a view — previously only column layout was applied, so reloading a page with a preset would show unfiltered data
Improved
- Token refresh endpoint is more resilient — no longer returns intermittent 500 errors when organisation role is missing from the token
2026-03-12
Added
- About, Careers, and Status pages on the public website
- System health endpoint (
GET /api/status) — check the status of all backend services with latency measurements - "Manage Account" and "Workspace Overview" links in the spread app user menu for quick navigation to the admin app
- Recent organisation/workspace autocomplete on the spread login form, powered by localStorage history
- API documentation now links to the MCP integration guide and mentions per-workspace Swagger UI and OpenAPI spec endpoints
Improved
- Spread login form redesigned to match the admin login (Material card, OAuth buttons, form field icons)
- Admin and website footers unified with the same four columns: Product, Resources, Company, Legal
- Footer links now point to real pages instead of placeholders
- "Contact Us" footer link now uses the correct support email address
- API documentation landing page (
/api/) no longer returns a 404
Removed
- "Remember me" checkbox removed from both login forms (was not functional)
2026-03-11
Added
- Billing email address — set an optional billing email on your organisation that's used for invoices and payment communications, separate from your primary organisation email
- Country selector — organisation country is now selected from a searchable dropdown of standardized ISO countries instead of free-text input
- Public countries endpoint (
GET /api/countries) — returns all available countries with ISO codes for use in dropdowns - GDPR consent tracking — registration, invitation acceptance, and OAuth first login now require explicit acceptance of terms of service and privacy policy, with optional marketing consent
- Billing history — view past invoices with order numbers, amounts, and download links on the Plan & Billing page
- Cancel subscription confirmation — cancelling a subscription now requires explicit confirmation via a dialog
Improved
- Checkout now pre-fills your billing address (country, postal code) and tax number when starting a subscription
- Customer records at the payment provider now include your organisation's city and country
- Existing free-text country values have been automatically migrated to standardized country codes
- Billing integration — real API endpoints for subscription, checkout, cancel, invoices, and billing portal replace mock data
- Invoice styling — order numbers, teal avatars, hover states, and consistent box-shadow across billing history
- Eliminated M3 default tertiary green — all success/active states now use the app's proper teal/green palette in both light and dark mode
- Organisation email is now required and cannot be left blank
2026-03-10
Added
- Billing and subscription management — subscribe to a plan, view your current subscription, cancel or resume, and access the billing portal directly from the app
- Provider-agnostic payment architecture — payment processing works through LemonSqueezy today with built-in support for switching to Stripe or other providers in the future
- Webhook-based subscription sync — plan changes, cancellations, and payment events are automatically reflected in your account via secure webhook processing
Improved
- Dark mode styling across the admin app — plan cards, alert banners, empty states, input fields, and member tables now render correctly in dark mode
- Workspace settings button shows a label on desktop and an icon-only cogwheel on mobile
- Pages now scroll to the top when navigating between routes
- Brand logo in the toolbar now links to the dashboard
- Fixed duplicate workspaces appearing briefly after creating a new workspace
- Fixed workspace delete failing with "Method Not Allowed" when the workspace was loaded from the list endpoint
- Data platform app is now mobile responsive — hamburger menu with sidebar navigation, compact toolbar with filter and preset controls, and the properties panel overlays content instead of squeezing it on small screens
- Tapping outside the properties panel on mobile now closes it
- View permissions dialog hides the source column on mobile for a cleaner layout, and shortens the "Add Member" button to "Add"
- Real-time workspace events (create, update, delete, status change) now include echo prevention so the originating browser tab won't show redundant notifications
2026-03-08
Added
- Sign in with Google or GitHub — use your existing account to log in or register with one click, no password needed
- Security alert emails — you now receive email notifications when sensitive account actions occur: password changes, email changes, two-factor authentication toggles, and API/MCP key operations
- New device login detection — when you log in from a device you haven't used before, you'll receive a security alert email with device and IP details
- Known devices tracking — SchemaStack remembers your devices and only alerts on truly new ones
- Send Feedback link in the admin footer — quickly share feedback without leaving the app
Improved
- New organisations now land on the workspaces page instead of an empty dashboard, making it easier to create your first workspace
- Error messages on login and registration pages now clear automatically when you navigate between pages
- Organisation settings button on the dashboard is now only visible to admins and owners
- More consistent and reliable error responses across all API endpoints — errors now always return a JSON
{"error": "..."}format with correct HTTP status codes - MCP
add_columntool now accepts widget types (STRING, EMAIL, NUMBER, DATE, etc.) instead of raw database types — matches the same experience as the UI - Feedback screenshots are now stored in S3 with auto-generated thumbnails instead of inline base64 in the database
- Auto-login after email verification — you're signed in immediately after confirming your code, no extra login step needed
- Auto-login after accepting an invitation — new and existing users are signed in and taken straight to the organisation
- Email verification now uses a 6-digit code instead of a clickable link — enter the code directly in the app without leaving the page
- Verification codes expire after 10 minutes with a maximum of 5 attempts for added security
- Resend verification is rate-limited to one request per 60 seconds to prevent spam
2026-03-07
Added
- API key rotation — regenerate the secret for a workspace API key or MCP API key while keeping its name, permissions, access mode, view scopes, and expiration intact. The old key stops working immediately
- Organisation-level SSE stream — a single real-time connection per organisation delivers workspace lifecycle events, member changes, config updates, and API/MCP key events to the admin app
- Real-time notifications when workspaces are created or deleted within your organisation
- Real-time notifications when organisation members are added, updated, or removed
- Real-time notifications for workspace configuration changes (API keys, MCP keys, MCP config, API config, storage config)
- Rich onboarding empty state when you have no workspaces yet — includes feature highlights, benefits overview, and a quick-start button
- Welcome screen when a workspace has no views, with a one-click "Create View" button
- MCP (Model Context Protocol) support — AI clients like Claude Desktop can now interact with your workspaces through the standard MCP protocol
- Per-workspace MCP access control with four levels: Disabled, Read-Only, Data-Only, and Full
- 18 MCP tools covering workspace browsing, view/column management, data querying, record creation/editing, and constraint management
- New MCP config endpoints to manage per-workspace access settings
- MCP API keys — dedicated API keys for AI agents to connect via MCP without a browser session. Create, list, and revoke keys per workspace
- Per-key MCP access control — each MCP API key can have its own access level (Read-Only, Data-Only, Full) independent of the workspace default, and can be scoped to specific views with per-view roles (Viewer, Editor, Admin)
Improved
- Slug fields now show a live URL preview (e.g.
schemastack.io/my-org/my-workspace) instead of generic hint text when creating organisations, workspaces, and views - Platform upgraded to Quarkus 3.30 for improved performance and compatibility with the MCP server extension
2026-03-06
Added
- Create your own organisation after registration — no invitation required
- Check organisation slug availability before creating an organisation
- Check workspace slug availability within an organisation
- Check view slug availability within a workspace
- Schema options (nullable, unique, length, precision, scale, default value) can now be set directly when creating a new column
- Timestamp auto-set behavior (created_at, updated_at) configurable in both the add-column dialog and column properties panel
Improved
- New SVG logo replaces the text lettermark across the entire app for a sharper, scalable brand identity
- SVG favicons added for both admin and app
- Dialog widths standardized across all dialogs for a more consistent experience
- Column type selector now shows icons alongside type names for easier scanning
- Workspace overview shows a prominent call-to-action when no workspaces exist yet
- All documentation and help links now point to docs.schemastack.io
2026-03-05
Improved
- File uploads and thumbnail generation moved to dedicated JVM worker for better performance with large files and improved reliability
- Replacing a file or image in a cell now automatically deletes the old file from storage — no orphaned files left behind
2026-03-04
Added
- Column constraints (e.g., NOT_BLANK, MAX_LENGTH, EMAIL, PATTERN) can now be sent inline when creating a column, removing the need for separate API calls
- Default value support in advanced column options
- Visibility and access controls (API visible, sortable) can now be set when creating a column
Improved
- Default values are now validated against the column type before saving — invalid combinations (e.g.,
now()on a boolean) are rejected with a clear error message - Default value input now shows type-aware placeholders, contextual hints, and quick-insert chips for common defaults (e.g.,
true/falsefor checkboxes,now()for timestamps)
2026-03-03
Added
- Owner crown badge on member tables across organization, workspace, and view permissions
- Inline "Pending" badge for invited members in the organization members list
- Joined date shown on all member rows (e.g., "Joined Mar 2026")
- Summary stat cards below member tables: Active Members, Pending Invites, and Admin Users
- Reusable alert banner component with warning, error, info, and success variants
- "Current Usage" overview section and "Usage by Workspace" breakdown on the Plan & Billing page
- View column properties panel with description editing
Improved
- Access mode dialog redesigned with colored icon avatars, capability chips, "Selected" badge, and blue info box
- Dialog headers updated with larger icons, bolder titles, and subtle gradient in dark mode
- Consistent green/red capability chips across the app with proper dark mode colors
- Blue info boxes now have explicit light and dark mode styling
- Form field backgrounds now properly adapt to dark mode
- Gray borders unified across the app for a cleaner look
- Plan & Billing page redesigned: active plan card is visually elevated with teal-green border and badges, Enterprise card has a dark premium look with golden crown
- Consistent elevation styling across header cards, member tables, and plan cards
- Standardized lighter gray for secondary/metadata text across all admin pages
- Typography utility classes (
.mat-display-largethrough.mat-label-small) now correctly generated in compiled CSS - Member avatars updated to 40px rounded squares for a modern look
- Workspace overview tabs no longer stretch to full width
2026-03-01
Added
- Widget options framework: per-widget-type configurable settings stored as JSONB on view columns
- DATETIME auto-set timestamps: configure columns to auto-fill
CURRENT_TIMESTAMPon insert, update, or both — applied at the database level viaDEFAULTand triggers - S3 storage backend: configure per-workspace S3 credentials (AWS S3, MinIO, DigitalOcean Spaces, Backblaze B2) for file and image storage instead of local filesystem
- S3 connection testing: verify bucket access before saving configuration
- Smart file mapping: map existing S3 files to FILE/IMAGE columns using path templates with row-level expressions (
${value},${row.column_name},${workspace.uuid}) - Entity-level default file path template: set a fallback S3 template for all FILE/IMAGE columns in an entity
- Column-level S3 path template: override the entity default for individual columns
- Presigned URLs enabled by default: file downloads redirect directly to S3, reducing API bandwidth
- Thumbnail presigned URLs served from platform S3 for faster image previews
hasStorageConfigfield on workspace membership endpoint so the frontend knows when S3 is configured- Proxied mapped file downloads: seamless download of template-resolved S3 files through the existing file download endpoint
- File path templates configurable at 3 levels: workspace (admin), view, and column (IMAGE/FILE columns only), with collapsible variable reference and live preview
- Shared widget components for file upload and simple inputs, reused across cell-edit popover and add-row dialog
- IMAGE/FILE upload support in the add-row dialog
Fixed
- Column schema migrations (widget options, type changes, constraints) now correctly use the actual database column name instead of the display name
- SQL identifiers in schema migrations are now properly quoted, preventing errors with column names containing spaces or special characters
Improved
- New columns created through the UI now use sanitized database column names (e.g., "Created at" →
created_at) instead of storing the display name - Renamed column-level
s3PathTemplatetofilePathTemplatefor consistency across all levels (column, entity, workspace) - Managed files (uploaded through SchemaStack) now always resolve correctly even when a file path template is configured on the column
- File path template changes require explicit "Apply" instead of auto-saving, preventing interference with in-progress uploads
- Warning shown when changing path templates that existing files will not be moved
- Storage configuration check now uses the workspace member endpoint instead of a separate API call
Security
- S3 credentials encrypted at rest using AES-256/GCM (same encryption as database credentials)
- S3 secrets are write-only in the API — never returned in responses
- Path traversal protection in S3 template resolution
2026-02-28
Added
- Real-time SSE updates for workspace events: status changes, settings updates, member changes, and database configuration now sync instantly across all open tabs and users
- Real-time view access updates: changes to view permissions (addable, editable, exportable) are reflected immediately for all collaborators
- Real-time view member and guest link changes are broadcast to all connected users
- Plan limit enforcement: workspace, view, and member creation is now blocked when your subscription tier limit is reached, with clear error messages showing the current count and maximum allowed
- Subscription tiers (Free, Pro, Enterprise) with configurable plan limits for workspaces, views, members, API calls, and storage
- Usage tracking: monthly request counts, row reads/writes, and storage are recorded per workspace
- Usage & Subscription API: view current plan, usage summary, and per-workspace breakdown via
/api/subscription - Rate limiting for the workspace API: per-minute burst limits and monthly request quotas, with standard
X-RateLimit-*response headers - Per-workspace and per-entity rate limit overrides configurable from the admin app
- SSL/TLS support for external database connections: configure SSL mode (Disable, Prefer, Require, Verify CA, Verify Full), upload CA and client certificates for secure connections to cloud-managed databases (AWS RDS, Azure, DigitalOcean, Google Cloud SQL)
- Connection timeout setting for external database connections
- Categorized connection test errors (Network, Auth, SSL, Timeout) for clearer troubleshooting
- Workspace-level API settings: configure max expand depth and CORS allowed origins per workspace
- Per-entity API settings: manage default expand, field selection, expandable relationship whitelists, and filterable field whitelists from the admin app
- Column API visibility: hide sensitive columns from REST API responses while keeping them visible in the admin UI
- Public access for entities: configure unauthenticated access (None, Read Only, or Read & Write) per entity
- Dynamic CORS: per-workspace origin restrictions replace the previous allow-all default
- Workspace-level max expand depth acts as fallback when entity-level is not configured
Security
- Upgraded encryption from AES/ECB to AES/GCM for database credential and 2FA secret storage (existing encrypted values are automatically decrypted via backward-compatible fallback)
- SSL certificate temp files are now cleaned up when workspace metadata is evicted from cache instead of accumulating until shutdown
Improved
- Workspace API URLs now use dashed slugs (e.g.
/column-metadata) matching org and workspace URL conventions - Swagger/OpenAPI sandbox lists entities with dashed slug URLs
- SSE connection status banners now show when real-time connection is lost or reconnecting
- Offline detection banners in both Admin and Data Platform apps
- Confirmation dialogs for all destructive actions (replaces browser confirm prompts)
- SSE reconnection uses jitter to prevent thundering herd on server restarts
Fixed
- Production environment detection now uses Angular's built-in
isDevMode() - CI/CD pipeline build paths and linting
- Console logging stripped from production builds
Fixed
- SSE disconnect banner now matches the height of other status banners
- View permissions toggles (addable, editable, exportable) no longer break after the first change
- Workspace status changes now update the status banner in real time
- Retry logic for newly created views no longer silently drops the final error