<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: informat</title>
    <description>The latest articles on DEV Community by informat (@informat).</description>
    <link>https://dev.to/informat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3901489%2Fab8d13c5-0932-420d-9f96-849a0743c7cd.png</url>
      <title>DEV Community: informat</title>
      <link>https://dev.to/informat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/informat"/>
    <language>en</language>
    <item>
      <title>Row-Level Security Is the Easy Half. Field-Level Security Is the Hard One.</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Sun, 04 Oct 2026 01:37:23 +0000</pubDate>
      <link>https://dev.to/informat/row-level-security-is-the-easy-half-field-level-security-is-the-hard-one-41a6</link>
      <guid>https://dev.to/informat/row-level-security-is-the-easy-half-field-level-security-is-the-hard-one-41a6</guid>
      <description>&lt;p&gt;Two years ago, in a quarterly review, the HR director of a manufacturing customer asked me a question and I answered it too fast. "Everyone on the plant floor can open a personnel record," she said. "Only three people should ever be able to see the salary field. Can your platform do that?"&lt;/p&gt;

&lt;p&gt;I said yes. It took us eighteen months to find out what I had actually promised.&lt;/p&gt;

&lt;p&gt;The demo was easy. We added a setting on the field: hidden for these roles. The form rendered, the field disappeared, everyone nodded. That demo worked perfectly, and it kept working perfectly, right up until the customer's finance team asked for an export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rows have owners. Columns have meanings.
&lt;/h2&gt;

&lt;p&gt;Row-level security is the part of the problem that behaves. A row has an owner — a department, a region, a creator, a customer. There is usually a natural key you can attach a rule to, and that rule can be pushed into the query as a predicate. Filtering rows is a database problem, and databases are good at database problems.&lt;/p&gt;

&lt;p&gt;A column has no owner. A column has a meaning. And the meaning of a column is a decision somebody made eighteen months ago, in a meeting nobody recorded, by a person who has since left the company.&lt;/p&gt;

&lt;p&gt;That asymmetry is why almost every low-code platform ships row-level security first and field-level security later, and why the second one leaks quietly for years before anyone notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is not the form. It is every other reader.
&lt;/h2&gt;

&lt;p&gt;Field-level security fails because it gets implemented as a property of a screen instead of a property of the model. A screen is just one reader. A successful platform has many, and you add more every quarter.&lt;/p&gt;

&lt;p&gt;Here is what we already had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The list view.&lt;/strong&gt; We hid the field on the detail form. The grid still rendered thirty columns, and the salary was one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The export.&lt;/strong&gt; Export never touches the form. It runs against the query layer with its own serializer, written by a different person in a different year with a different idea of what "hidden" meant. The field was masked on screen and raw in the CSV. The HR director found out the way customers always find out: someone emailed a spreadsheet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The filter panel.&lt;/strong&gt; This is the one I am still embarrassed by. A user who could not see the field could still filter and sort by it. So the hidden field became an oracle. You cannot read the salary column, but you can sort by it and see who is at the top. You can filter for values above a threshold and bisect the range until you know what your colleague earns. We had not merely failed to hide the data. We had published a query interface to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API.&lt;/strong&gt; The field was gone from the form, and fully present in the API response, because our serializers were designed to be complete. Completeness is a virtue in a serializer and a vulnerability in a permission model. Anyone with a token and a browser console could read what the form politely declined to show.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The search index.&lt;/strong&gt; The index is built by a background job running as a system account with no user context at all. Query-time permission checks made the search results look honest. But the index itself held the raw value, and the highlighting feature — which existed only to be helpful — would happily show a sentence fragment containing the number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dashboard.&lt;/strong&gt; You cannot see individual salaries, but you can see the sum for a region. Subtract the values you already know, and three people's salaries remain. Aggregation does not hide data. It turns it into a puzzle with a known answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The notification.&lt;/strong&gt; Message templates are usually built in a drag-and-drop designer by somebody in operations, not by an engineer. A template renders server-side, before anyone asks who the recipient is. The notification is a second renderer, built by a non-engineer, without any of the guardrails of the first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The audit log.&lt;/strong&gt; Our audit trail was, and still is, excellent. It records every field, before and after, immutably. Then we asked who could read the audit log. It was a considerably larger group than the group allowed to see the field. The history of a value is the value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI agent.&lt;/strong&gt; This is the one that finally forced the redesign. An agent queries as itself, not as you. We gave it a service account, and service accounts carry no field restrictions, because service accounts are system actors. So the agent reads the salary column and then, being helpful, summarizes it.&lt;/p&gt;

&lt;p&gt;Nine readers. We had hidden the field in one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Masking is not encryption, and neither one is a policy
&lt;/h2&gt;

&lt;p&gt;Whenever this came up, someone suggested encrypting the field. Encryption at rest protects the disk. It does not protect the column. An encrypted value that the platform decrypts in order to show it to an authorized user is, from the platform's point of view, simply a value — and every reader listed above will receive the decrypted version.&lt;/p&gt;

&lt;p&gt;Masking is a presentation decision. Rendering a phone number as its last four digits is a formatting choice. It removes nothing. It changes what a cell looks like and nothing else about what the system knows.&lt;/p&gt;

&lt;p&gt;Neither one is a policy. A policy is a single answer to a single question — who may read this field — and it has to be the same answer in every reader. If five readers each implement the rule, you do not have a rule. You have five habits, and four of them will drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed
&lt;/h2&gt;

&lt;p&gt;The fix was structural, and it moved work backwards into the metadata layer.&lt;/p&gt;

&lt;p&gt;Field sensitivity became a property of the field in the model, not a setting on a form. The policy is resolved into the query, so a hidden field is absent from the result set rather than removed from the render. Filters, sorts, grouping and tree views consult the same policy, because they all pass through the same resolver. Exports go through it too, and producing an export now writes its own audit event — a different event from viewing a record, because it is a different act.&lt;/p&gt;

&lt;p&gt;The search index no longer contains fields the index is not allowed to contain. It is less useful than it was. It is also no longer a leak.&lt;/p&gt;

&lt;p&gt;The AI agent runs with the asking user's scope, not its own. That single change altered more behavior than anything else we did.&lt;/p&gt;

&lt;p&gt;And we stopped masking silently. If you cannot see a field, the field says so. A blank cell is a lie when it looks like "no data."&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;The row is the unit your users argue about. The column is the unit your auditors argue about. We built field-level security as a checkbox on a form because that is where the customer asked the question — and it was the right feature in the wrong layer.&lt;/p&gt;

&lt;p&gt;Here is the part I keep coming back to. The number of readers of a field grows with the platform's success. Every capability you ship — export, search, dashboards, notifications, integrations, an AI agent — is another reader, and each one is built by a different team in a different year.&lt;/p&gt;

&lt;p&gt;So field-level security is not a feature you finish. It is an invariant you maintain, and the cost of maintaining it scales with the number of ways your platform can read a single value.&lt;/p&gt;

&lt;p&gt;A permission system is judged by what it denies. A field security model is judged by what it forgets to deny.&lt;/p&gt;

&lt;p&gt;Ours looked complete. Then we shipped an AI agent and found out it wasn't.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>security</category>
      <category>privacy</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A Dashboard Is Not a Report. It Is an Argument About Definitions.</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Sat, 03 Oct 2026 04:17:47 +0000</pubDate>
      <link>https://dev.to/informat/a-dashboard-is-not-a-report-it-is-an-argument-about-definitions-4bp2</link>
      <guid>https://dev.to/informat/a-dashboard-is-not-a-report-it-is-an-argument-about-definitions-4bp2</guid>
      <description>&lt;p&gt;Lina runs operations for a furniture manufacturer in Foshan. Two years ago she sent me a screenshot at 7:40 in the morning: two dashboards, side by side, both titled "August Revenue." One said 41.2 million. The other said 36.9 million. Same company, same month, same platform.&lt;/p&gt;

&lt;p&gt;"Which one is right?" she asked.&lt;/p&gt;

&lt;p&gt;Both of them were right. That was the problem.&lt;/p&gt;

&lt;p&gt;I have spent a lot of time thinking about the exact moment a low-code platform stops being a builder and starts being a source of truth. It always happens at the dashboard. Forms and workflows are forgiving: if a field is ugly, one person suffers. A dashboard is where an entire company agrees, at once, on a number that will drive a decision — and in most low-code platforms, including the one I work on, the dashboard is the least governed, least versioned, least owned part of the system.&lt;/p&gt;

&lt;p&gt;That is backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number is not the problem
&lt;/h2&gt;

&lt;p&gt;Nobody argues about the arithmetic. The argument is always about the verb.&lt;/p&gt;

&lt;p&gt;For Lina, "revenue" meant money we have invoiced. For the finance lead, it meant money we have collected. For the sales director, it meant orders we have booked, including the ones not yet shipped. Three definitions, three legitimate business concepts, one word, and one chart title.&lt;/p&gt;

&lt;p&gt;Every extra dashboard multiplies the disagreement, because each chart carries a definition that lives only in the head of whoever dragged it together. When that person leaves, the definition leaves with them, and the chart keeps rendering. A number with no author is the most dangerous artifact a business system can produce.&lt;/p&gt;

&lt;p&gt;In a mature data stack, this is the metric layer's job: a named, owned, versioned definition of "revenue," consumed by every chart in the company. In a typical low-code platform, there is no metric layer. There is a chart component, a data table, and a person with an opinion. That is not a bug in the product. It is a design choice dressed up as flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the definitions actually live
&lt;/h2&gt;

&lt;p&gt;When I look at how our customers build dashboards, the logic is never in a metric. It is scattered across four places at once: the filter panel, the aggregation setting on the chart, a computed column in the data table, and — almost always — a script someone wrote two years ago and nobody has read since.&lt;/p&gt;

&lt;p&gt;That last one is where I lose sleep. A script is the most powerful answer to a business question and also the most invisible one. It runs, it produces a number, and it has no name. If it is wrong, it is wrong quietly, for everyone, forever. A filter that should have excluded cancelled orders is still syntactically correct; it just adds three million to the total.&lt;/p&gt;

&lt;p&gt;The honest fix is unglamorous: give metrics names, give them owners, give them a place to live that is not a chart's config panel. But the pitch for a low-code platform is speed, and a metric registry is friction. Every time we consider making it mandatory, someone points out — correctly — that the whole point is that a business analyst can answer a question without filing a ticket.&lt;/p&gt;

&lt;p&gt;The tension is real, and I have stopped pretending it resolves itself. Speed and correctness are not opposites. Unowned metrics are just debt, and low-code platforms are unusually good at letting you borrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The total will never equal the sum of the rows you can see
&lt;/h2&gt;

&lt;p&gt;Here is a small, specific thing that quietly destroys trust in more dashboards than any modeling mistake.&lt;/p&gt;

&lt;p&gt;A user sees a card: 41.2 million. They click it. The platform shows them a list of the underlying records, capped at 500 rows for performance. They add up the visible rows, get a different number, and conclude the dashboard is lying.&lt;/p&gt;

&lt;p&gt;It is not lying. The list was paginated and truncated; the total was computed in the database over the full set. But the user cannot see that. The same trap appears in top-N charts, where everything outside the top ten collapses into "Others," and in any chart that groups by a nullable field, because null silently becomes its own bucket with no label.&lt;/p&gt;

&lt;p&gt;None of these are hard problems. All of them are decisions someone has to make on purpose, and in a drag-and-drop builder the default is to make them by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Real-time" is a promise the architecture has to keep
&lt;/h2&gt;

&lt;p&gt;The documentation says the dashboard supports real-time data updates. It usually does — while the table is small.&lt;/p&gt;

&lt;p&gt;Then the table grows. A customer in manufacturing imports three years of work orders: four million rows. The dashboard that felt instant at fifty thousand rows now takes eleven seconds, and it takes those eleven seconds on every page load, for every user, against the primary database.&lt;/p&gt;

&lt;p&gt;Real-time is not a refresh interval. It is an architectural commitment: either you precompute aggregates and accept that they lag, or you compute live and accept that the database will eventually pay the bill. Most platforms quietly choose the second option and keep calling the result "real-time." The bill arrives later, as a slow platform, and it gets blamed on the platform's performance rather than on the dashboard's design.&lt;/p&gt;

&lt;p&gt;I have started telling customers the truth: a dashboard is a query. If the query is expensive, no amount of drag-and-drop will make it cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions turn one chart into many
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprises people. Two executives open the same chart and see different totals — and both are correct, because their data scopes differ. A regional manager should not see the national number. So the aggregate must be computed after permission filtering, not before.&lt;/p&gt;

&lt;p&gt;That single requirement destroys the naive version of pre-aggregation. You cannot sum all orders into one row and then apply permissions; permissions have to be applied to the rows before they are summed. The moment you accept that, you have a choice: precompute per scope, which explodes combinatorially, or compute live per user, which is expensive on large tables.&lt;/p&gt;

&lt;p&gt;I do not think there is a clean answer. I think there is only an honest one: decide which dashboards need to reconcile perfectly with row-level security and which ones are allowed to be approximate — and label the second kind out loud. Companies are far more tolerant of "this number is a snapshot from this morning" than of a number they cannot trust and cannot explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dashboard is where your data model is judged
&lt;/h2&gt;

&lt;p&gt;By the time a form is submitted, a shortcut in the data model is invisible. A dashboard exposes it.&lt;/p&gt;

&lt;p&gt;Currency stored as a string. Timestamps with no timezone. A status field that grew from five values to nineteen over two years. A denormalized total that was correct when it was written and has drifted ever since. None of this hurts while people fill in forms one record at a time. All of it hurts the moment someone asks for the sum of a column across four million rows, or needs two charts to agree on the same boundary condition.&lt;/p&gt;

&lt;p&gt;Dashboards do not create these problems. They are simply where the problems become visible, and then where they get blamed on the reporting layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;A dashboard is not a report. A report ends. A dashboard sits on a wall and becomes the number everyone uses, and the moment it does, it stops being a visualization and becomes a shared definition — which is to say, software.&lt;/p&gt;

&lt;p&gt;Every metric is a small program with an owner, a version, and a changelog. Most low-code platforms, including mine, let you create one in ninety seconds with no owner, no version, and no test. We call that empowerment.&lt;/p&gt;

&lt;p&gt;Lina's two dashboards are both still live. We did not delete either one. We gave each a name, a sentence of description, and a person who owns it — and then we watched the arguments change. Nobody asks which number is right anymore.&lt;/p&gt;

&lt;p&gt;They ask who decided, and whether that decision has changed.&lt;/p&gt;

&lt;p&gt;That is a better question. It is also, quietly, an admission that a metric is a piece of software — and that the most popular feature of a low-code platform may be the one that lets you ship software nobody has to maintain.&lt;/p&gt;

&lt;p&gt;Until Monday morning.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>analytics</category>
      <category>data</category>
      <category>architecture</category>
    </item>
    <item>
      <title>What Happens When the Import Fails Halfway?</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Fri, 02 Oct 2026 01:44:04 +0000</pubDate>
      <link>https://dev.to/informat/what-happens-when-the-import-fails-halfway-h23</link>
      <guid>https://dev.to/informat/what-happens-when-the-import-fails-halfway-h23</guid>
      <description>&lt;p&gt;On a Tuesday morning in March, the warehouse supervisor at a manufacturing customer called me. Not a ticket. Not an email. A phone call, which in our world means something is on fire.&lt;/p&gt;

&lt;p&gt;She had spent two weeks preparing the migration of eleven years of inventory records. Twelve thousand rows, cleaned by hand, in a spreadsheet her team had guarded like a religious document. The import had run. The progress bar had completed. The platform said "Import successful."&lt;/p&gt;

&lt;p&gt;Nine hundred rows were wrong. Dates had flipped between formats. A subset of rows had been skipped without any message. And the same three suppliers now existed four times each, because she had re-run the import after the first attempt seemed to stall.&lt;/p&gt;

&lt;p&gt;Her question was not technical. It was the question that has stayed with me since: "Why did it tell me it worked?"&lt;/p&gt;

&lt;p&gt;That day I learned that import is not a feature. It is a contract. And for most of the platforms I have worked on, including mine, we had written that contract in very small print.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import looks like a checkbox
&lt;/h2&gt;

&lt;p&gt;In every product plan I have ever seen, import is one line. "Excel import/export: yes." It sits in a feature matrix next to dark mode and CSV download. It demos beautifully: pick a file, watch a progress bar, done.&lt;/p&gt;

&lt;p&gt;But that one line hides an entire pipeline. Parse. Validate. Transform. Match against existing records. Deduplicate. Commit. Report. Every one of those stages can fail, and each one fails differently. The checkbox in the feature matrix does not tell you what happens between the progress bar and the database.&lt;/p&gt;

&lt;p&gt;Most platforms, mine included for too long, treat the middle of that pipeline as an implementation detail. That is exactly backwards. The middle is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first decision nobody makes explicitly
&lt;/h2&gt;

&lt;p&gt;The first real design decision in any import system is failure semantics. Do you commit everything, or nothing, or the rows that happened to pass?&lt;/p&gt;

&lt;p&gt;All-or-nothing is honest but brutal. Nine hundred bad rows out of twelve thousand means the customer fixes the spreadsheet and runs the whole job again, possibly several times, while the business waits. Best-effort is kinder but dangerous: the import succeeds, the failures are buried in a downloadable log the user will never open, and the data model quietly rots.&lt;/p&gt;

&lt;p&gt;We chose best-effort once because it demoed better. Then we spent a week helping the same customer discover which rows had silently failed, by comparing exports against their original spreadsheet. Never again.&lt;/p&gt;

&lt;p&gt;The answer, we eventually learned, is neither. It is staged: validate everything first, show the customer exactly what will happen, and only then commit. Which brings up the feature nobody asks for and everybody needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview is the whole product
&lt;/h2&gt;

&lt;p&gt;A dry-run mode — "show me what this import would do without actually doing it" — is the single highest-leverage feature in this space. It costs a fraction of the effort of the rest of the pipeline, and it changes the emotional experience completely. An import with preview is a negotiation. An import without preview is a gamble.&lt;/p&gt;

&lt;p&gt;When we added preview, something unexpected happened. The support tickets did not just drop. The conversations changed. Customers started bringing us their edge cases before running the import, instead of after. "What happens to this row? Is this supplier the same as that one?" Preview turned the import from an oracle into a document.&lt;/p&gt;

&lt;p&gt;Matching is where most of the pain lives anyway. Does the row with supplier name "ABC Co." match the existing record "ABC Company Ltd."? Exact string comparison says no. A human says obviously yes. Whatever rule you pick, showing it in a preview lets the customer disagree with the machine before the machine makes the mistake permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Big files are async jobs, whether you planned it or not
&lt;/h2&gt;

&lt;p&gt;The second thing product plans get wrong is scale. In the demo, the import file has forty rows. In production, someone eventually drops in two hundred thousand rows exported from a system older than your platform.&lt;/p&gt;

&lt;p&gt;At that size, an import is not a form submission. It is a background job with its own lifecycle: queued, running, partially done, failed, retried. It needs its own status page. It needs to survive the user closing the browser. It needs to not time out the web server, and not block every other user of the tenant while it chews through the file.&lt;/p&gt;

&lt;p&gt;We learned this the expensive way, when a customer's import of historical sales data locked their workspace for the better part of an afternoon. The lesson was not "make imports faster." The lesson was: the moment a batch operation can outlive an HTTP request, it must be designed as a job, with progress, with logs, and with a way to cancel it that actually cancels it.&lt;/p&gt;

&lt;p&gt;And it must be idempotent. She re-ran the import because it seemed stalled. She should have been able to re-run it safely. Any batch operation in a business system will be re-run — because someone is unsure, because someone got impatient, because someone closed the laptop on a train. Design for the second run, not just the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export is the same problem wearing a nicer shirt
&lt;/h2&gt;

&lt;p&gt;We tend to think of export as the polite sibling. It takes nothing from you. What could go wrong?&lt;/p&gt;

&lt;p&gt;An export is a query with a download button, and every failure mode of a query applies. Large exports time out. Exports that stream hold database connections. Exports of filtered views race against concurrent edits. And the file the customer downloads becomes a snapshot that lives in spreadsheets, in email attachments, in places your permission system has never heard of.&lt;/p&gt;

&lt;p&gt;Also, encoding. I will simply say: if you have never watched a customer open an export full of unreadable characters because their office computer opened your CSV with the wrong code page, you have not yet supported enterprise software in the wild.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth about export is that it is the last thing the customer keeps when they are leaving your platform. The quality of your export decides how gracefully they can go — and, oddly, caring about that is one of the strongest trust signals you can send.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trust gradient
&lt;/h2&gt;

&lt;p&gt;Here is the pattern I now believe: trust in a low-code platform does not arrive through the modeling tools. It arrives through the data plumbing.&lt;/p&gt;

&lt;p&gt;A customer can forgive a form designer that feels clunky. They do not forgive a database that silently dropped four hundred rows. Every modeling feature is about the future, and the future is negotiable. Data integrity is about the past, and the past is not.&lt;/p&gt;

&lt;p&gt;The import button is the moment the customer's actual history enters your system. Everything before it is a demo. Everything after it depends on how honestly you handled their eleven years of inventory records.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;We spent years making low-code modeling visual, drag-and-drop, approachable. We put that in the pitch deck. Nobody ever put the import pipeline in a pitch deck.&lt;/p&gt;

&lt;p&gt;But adoption is decided in the unglamorous places. The customers who stay are the ones whose data made it in intact, whose failed rows came back in a report they could read, whose re-runs did not duplicate anything, whose exports opened correctly on the office computer from 2013.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is what that implies about where engineering effort should go. The wizard with the progress bar demos in ninety seconds. The staging, the preview, the job system, the idempotency, the error reports — that is months of invisible work. There is no badge for it, no screenshot for the landing page.&lt;/p&gt;

&lt;p&gt;We ship the wizard first anyway, every time, because it demos well. And then a supervisor with eleven years of records calls on a Tuesday, and we learn the same lesson again.&lt;/p&gt;

&lt;p&gt;The import is never just a feature. It is the moment your platform becomes responsible for someone's history. Design like it.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>data</category>
      <category>architecture</category>
      <category>enterprise</category>
    </item>
    <item>
      <title>The Search Box Is the Most Expensive Feature Nobody Asked For</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Thu, 01 Oct 2026 01:59:26 +0000</pubDate>
      <link>https://dev.to/informat/the-search-box-is-the-most-expensive-feature-nobody-asked-for-32en</link>
      <guid>https://dev.to/informat/the-search-box-is-the-most-expensive-feature-nobody-asked-for-32en</guid>
      <description>&lt;p&gt;Two years ago, an operations manager at a logistics customer asked me for a feature, and I remember feeling relieved, because it sounded small.&lt;/p&gt;

&lt;p&gt;She said: I do not need another report. I need to type a phone number into one box and see everything that has ever touched that customer.&lt;/p&gt;

&lt;p&gt;Then she pointed at our product, which by then had thirty-four list views spread across eleven applications, each with its own filter panel, and asked the follow-up question that ended the meeting early: why do I have to know which table a thing lives in before I am allowed to look for it?&lt;/p&gt;

&lt;p&gt;We shipped a global search box about eight months later. It turned out to be the most expensive feature we built that year, and it appeared in exactly zero of our requirement documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everyone scopes the filter. Nobody scopes the search.
&lt;/h2&gt;

&lt;p&gt;A filter and a search box look identical in a screenshot and are opposites at the architecture level.&lt;/p&gt;

&lt;p&gt;A filter is a structured question. The user already knows the field, the operator, and the value: status is pending, created after Monday, owner is in my team. The platform answers it with a predicate, and the query engine we had already built handles it without breaking a sweat. Filtering has known inputs — which is precisely why every grid on the platform has one, and why we had thirty-four of them.&lt;/p&gt;

&lt;p&gt;A search box is an unstructured question. The user holds a fragment: a phone number with the punctuation wrong, half a company name written in the other language, an invoice number read off a photo, a person's nickname that exists in no schema anywhere. Now the platform has to decide which fields to look in, how to compare them, and what "close enough" is allowed to mean. Nobody knows the shape of the answer until it arrives.&lt;/p&gt;

&lt;p&gt;That difference is exactly why search never survives planning. In a scoping meeting, a filter is a checkbox next to an existing grid. A search box is "a text input and a LIKE query." Both sound like a day of work. One of them is.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are three corpora, not one
&lt;/h2&gt;

&lt;p&gt;The first corpus is records: every row, in every table, in every application, including the ones the user has never opened. The second is metadata: field labels, form names, workflow definitions, script names, permission rules. Builders search for things they built six months ago and cannot name, which is a completely different query against a completely different store. The third is unstructured content: attachments, scanned invoices, comments, chat threads pulled in from WeCom or DingTalk.&lt;/p&gt;

&lt;p&gt;One box queries all three, and users do not distinguish between them. But the three have different tokenizers, different permission rules, different retention policies, and vastly different cost per document. Every one of those differences has to be resolved before the box can be honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions, again, this time inside an index
&lt;/h2&gt;

&lt;p&gt;I have written before about how a responsive layout can quietly become a data exfiltration path. Search is the same mistake with a bigger blast radius, and it is easier to make.&lt;/p&gt;

&lt;p&gt;The moment you build an index, you have copied data out of the system of record and into a second system that has never heard of your permission model. If that index cannot answer "what is this user allowed to see," the search box becomes the most efficient leaking tool your platform will ever ship. One query and the results are ranked, paginated and highlighted for you.&lt;/p&gt;

&lt;p&gt;You get two ways out and both cost something. You can filter at query time, which is safe and pushes your permission logic into the hot path of the search request. Or you can scope the index itself, which is fast until you remember that permission scopes are combinations of tenant, role, department, and record ownership, and that combinations do not shard politely.&lt;/p&gt;

&lt;p&gt;Then there are two leaks nobody writes a ticket for. The result count — "12 results" tells you something even when you cannot read a single one of them. And the snippet: highlighting a match inside a field the user has no right to read is not a near miss, it is a disclosure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A search box is a language problem wearing a database costume
&lt;/h2&gt;

&lt;p&gt;Chinese does not have spaces. Tokenizing it is a different pipeline from tokenizing English, and the two have to coexist in the same index, because half our customers type both in the same working day. N-grams solve the segmentation problem and immediately create a noise problem, where a two-character fragment matches a fifth of the table. Stemming in one language does nothing in the other.&lt;/p&gt;

&lt;p&gt;And then there is the class of things that are not words at all: order numbers, tax IDs, phone numbers. Those should never be tokenized as text. They should be normalized and indexed as identifiers, because people type 138 0013 8000 and expect the platform to have already forgiven the spaces. Which normalization rules exist is a product decision wearing a technical costume, and somebody has to own it in writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freshness, backfill, and the tenant that arrives on Friday
&lt;/h2&gt;

&lt;p&gt;Search gives you a second system of record, and a second system of record gives you a consistency problem you did not have yesterday.&lt;/p&gt;

&lt;p&gt;Writes land in the database and the index separately, so search is eventually consistent whether you admit it or not. Users expect search-after-save, and someone who saves a record and cannot find it thirty seconds later will file a bug that reads like data loss. You need a staleness budget, stated in the interface, and you need to hold it.&lt;/p&gt;

&lt;p&gt;Then there is onboarding. Turning search on for an existing customer with forty million rows is not a configuration change; it is a backfill project with a migration window. And under multi-tenancy every new tenant means a new index, which means index provisioning is now part of tenant provisioning, and tenant deletion has to reach into the index too. A deletion request you cannot honor in the search layer is not a feature gap. It is a compliance incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI agent made search load-bearing
&lt;/h2&gt;

&lt;p&gt;When we let an AI agent query tables, we were not adding a chat interface. We were promoting our retrieval layer to the position of ground truth. The agent does not browse. It searches, reads what comes back, and then acts on it. If search returns the wrong customer, the agent does not look confused — it looks confident, and it writes the wrong thing into the wrong record.&lt;/p&gt;

&lt;p&gt;Search quality used to be a matter of user patience. Now it is an input to automation. Every ranking bug is a business decision bug, made with nobody watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed
&lt;/h2&gt;

&lt;p&gt;Mostly unglamorous things.&lt;/p&gt;

&lt;p&gt;We stopped treating search as a text input and started treating it as a subsystem with its own latency and freshness targets, funded like one. We split metadata search from record search, because they have different lifecycles and pretending otherwise made both slower. We moved permission resolution ahead of index access, so the index never decides what a user may see, and we deleted a highlighting path that could render a field the reader could not open. We declared normalization rules as metadata instead of burying them in the query builder.&lt;/p&gt;

&lt;p&gt;And we handed ranking to a product owner, on the grounds that choosing which of six plausible results is the right one is a business judgment, not a query-tuning exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;Here is what I believe now.&lt;/p&gt;

&lt;p&gt;We spent years perfecting the filter, because a filter demos beautifully: click a dropdown, watch the rows narrow, everybody nods. Search does not demo. You type something and the product either knows it or it does not — and in that one moment it exposes your data model, your permissions, and your plumbing all at once, with no slides in between.&lt;/p&gt;

&lt;p&gt;Which is why the search box is the most expensive feature nobody asked for, and why it is the most honest thing you will ever ship.&lt;/p&gt;

&lt;p&gt;Everyone asked us for reports. The operations manager who asked for search was the only one describing the actual job.&lt;/p&gt;

&lt;p&gt;A platform is not judged by what it can display. It is judged by what it can find.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>search</category>
      <category>database</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your Low-Code Platform Was Designed for Someone With a Desk</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Tue, 29 Sep 2026 01:37:24 +0000</pubDate>
      <link>https://dev.to/informat/your-low-code-platform-was-designed-for-someone-with-a-desk-28k1</link>
      <guid>https://dev.to/informat/your-low-code-platform-was-designed-for-someone-with-a-desk-28k1</guid>
      <description>&lt;p&gt;A year and a half ago, a manufacturing customer sent us a screenshot instead of a bug report.&lt;/p&gt;

&lt;p&gt;It was a phone. The screen showed one of our detail forms, rendered exactly as it renders on a 27-inch monitor, shrunk until the labels were unreadable. On top of it, someone had used a thumb to cross out half the fields. Below the screenshot, one sentence: our warehouse people use this all day, and they hate it.&lt;/p&gt;

&lt;p&gt;They did not ask for a mobile app. They assumed they already had one.&lt;/p&gt;

&lt;p&gt;That screenshot is still the most useful product feedback we have ever received, and it was never really about a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every demo is a laptop
&lt;/h2&gt;

&lt;p&gt;Nobody ships a low-code platform demo on a phone. That is not laziness; it is that the phone is a terrible stage. A form designer, a workflow canvas, a permission tree, a dashboard — these are instruments that need a wide surface, and they are exactly what you show when you are explaining what the product &lt;em&gt;is&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So the mental model of the platform, from the earliest architecture conversation, is built around a person sitting down. A person with a keyboard, a mouse, ideally two monitors, and thirty uninterrupted minutes to configure a screen.&lt;/p&gt;

&lt;p&gt;Then the application gets deployed, and the person who actually uses it is standing in an aisle with a box in one hand.&lt;/p&gt;

&lt;p&gt;The gap between those two people is not a breakpoint. It is a different product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile is not a rendering target. It is a second view of the same metadata.
&lt;/h2&gt;

&lt;p&gt;The mistake we made — and the mistake I see in almost every platform — is treating mobile as a layout problem. Something CSS can fix. Add a media query, stack the columns, collapse the sidebar, ship it.&lt;/p&gt;

&lt;p&gt;That fails because a desktop screen and a phone screen are not the same information architecture at two scales. They are two different answers to the question: which of these forty fields does this person, in this moment, actually need?&lt;/p&gt;

&lt;p&gt;Look at a real detail view in an enterprise app. Identity fields, classification fields, financial fields, attachments, status, owner, related records, history. On a desktop, showing all of it is a courtesy. On a phone, showing all of it is an act of hostility. The person on the shop floor needs three of those. The person in finance needs a different three.&lt;/p&gt;

&lt;p&gt;Which means the platform has to answer something it has never had to answer: &lt;strong&gt;who is looking, and where are they standing?&lt;/strong&gt; That is not styling. That is metadata — a per-view, per-role, per-context statement of what matters.&lt;/p&gt;

&lt;p&gt;And once you accept that, the honest conclusion arrives quickly: the desktop layout you shipped for years was never designed either. It just had enough room to hide the fact that you never decided.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table is where everything collides
&lt;/h2&gt;

&lt;p&gt;Forms are the easy part. The data table is where low-code platforms actually live — a grid of rows and columns is the most-used screen in every enterprise system ever built, and it is the one thing a phone genuinely cannot do.&lt;/p&gt;

&lt;p&gt;Forty columns. Frozen headers. Inline editing. Cross-page batch selection. Column-level permissions. Every one of those is a promise made to someone on a laptop.&lt;/p&gt;

&lt;p&gt;There are three ways out, and I have watched all three get chosen.&lt;/p&gt;

&lt;p&gt;You can reduce. Pick the five columns that matter for the mobile context, name that as a mobile view, and let the rest be reachable through the detail screen. This works, and it forces an admission: most grids are used as three columns and a search box.&lt;/p&gt;

&lt;p&gt;You can rotate. Tell the user to hold the phone sideways. That is a real answer for a real minority of cases and a terrible default, and it is a tell that you have not decided anything.&lt;/p&gt;

&lt;p&gt;Or you can pan. Let the grid scroll sideways and watch people lose track of which row they are on within the first three columns. I have never met a user who liked it.&lt;/p&gt;

&lt;p&gt;We ended on the first option, and it cost us something real: someone has to decide, per application, which five columns. That decision cannot be automated away, because it encodes business judgment about a job we do not do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The permission question hiding inside the layout question
&lt;/h2&gt;

&lt;p&gt;This is the part that took me too long to see.&lt;/p&gt;

&lt;p&gt;When you strip a mobile screen down to three fields, you are not only choosing what to show. You are deciding what the platform believes the client needs to hold.&lt;/p&gt;

&lt;p&gt;If the reduction happens in the browser — the field is fetched and then hidden with CSS — you have reduced nothing. You have shipped every confidential column in the salary table to a phone that someone left in a taxi. Responsive design has quietly become a data exfiltration path, and no security review catches it, because from the API's point of view nothing changed at all.&lt;/p&gt;

&lt;p&gt;The only safe version of this is the version where the mobile context is declared at the metadata layer, resolved on the server, and reflected in the query the platform actually runs. Which means mobile support cannot be a front-end project. It has to reach the same layer permissions reach.&lt;/p&gt;

&lt;p&gt;We learned that one the expensive way. I would rather you learned it from this paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most of your mobile users will never open your app
&lt;/h2&gt;

&lt;p&gt;The other thing we underestimated: enterprise mobile use does not look like consumer mobile use.&lt;/p&gt;

&lt;p&gt;Very few of our customers' field users browse to an application. They open WeCom, DingTalk, or Feishu, they see a message, they tap it. The notification &lt;em&gt;is&lt;/em&gt; the interface. The form and the grid are what happens after the tap, and they have to survive being embedded inside someone else's webview, with someone else's navigation bar, someone else's font scaling, and someone else's back button.&lt;/p&gt;

&lt;p&gt;That reframes the whole story. Mobile is not primarily about screens. It is about whether your platform can create a task that arrives as a message, opens in one tap, gets completed in ninety seconds by someone wearing gloves, and pushes its result back into a workflow that has been waiting since six in the morning.&lt;/p&gt;

&lt;p&gt;Which is a workflow question wearing a mobile costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed
&lt;/h2&gt;

&lt;p&gt;Not much of it was glamorous. We stopped pretending the phone was a smaller laptop: the mobile context became a first-class piece of view metadata that a builder defines on purpose and the server resolves before it runs anything. The reduction moved server-side, and we deleted the responsive hacks that had been quietly leaking columns to clients that never needed them.&lt;/p&gt;

&lt;p&gt;We made every form and grid openable through a tokenized link, so it can live inside a chat message instead of behind a navigation tree. We measured cold start on real hardware, because a phone on factory Wi-Fi is not a laptop on fiber, and a two-second desktop load is a ten-second one in the aisle.&lt;/p&gt;

&lt;p&gt;And we stopped letting the builder decide alone. The mobile view gets previewed on an actual phone, inside the actual app the customer already uses. Not a device emulator in a desktop browser. That distinction has caught more mistakes than any review meeting we have ever held.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;Here is what I actually believe after eighteen months of this.&lt;/p&gt;

&lt;p&gt;Low-code platforms sell speed, and speed is measured in how fast you can build a screen. But the screen was never the thing that mattered. What mattered was that the screen got used — by a specific person, in a specific place, with whatever was already in their hands.&lt;/p&gt;

&lt;p&gt;We spent years compressing the distance between an idea and a working application, and almost no time on the distance between a working application and a person who is not sitting down.&lt;/p&gt;

&lt;p&gt;The desktop was never the default. It was just the only context we had bothered to model — and for most of our customers' users, it is not where the work happens. It is where the work gets reviewed.&lt;/p&gt;

&lt;p&gt;The person holding the box is who your platform is actually for. If your metadata has no way to describe them, you did not build an enterprise platform.&lt;/p&gt;

&lt;p&gt;You built a very good demo.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>mobile</category>
      <category>ux</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Hardest Feature in a Low-Code Platform Is the Upgrade</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Mon, 28 Sep 2026 02:06:58 +0000</pubDate>
      <link>https://dev.to/informat/the-hardest-feature-in-a-low-code-platform-is-the-upgrade-41m7</link>
      <guid>https://dev.to/informat/the-hardest-feature-in-a-low-code-platform-is-the-upgrade-41m7</guid>
      <description>&lt;p&gt;Two weeks ago we shipped a release. A small one by our standards: a reworked lookup field, a smarter default for the date control, and a permission check moved one layer earlier in the write path.&lt;/p&gt;

&lt;p&gt;By Thursday afternoon, a customer's warehouse application had stopped assigning storage locations. Nobody had touched that app in eight months. The person who built it had left the company in March.&lt;/p&gt;

&lt;p&gt;The release did exactly what its notes said it would. It also broke something we had never opened.&lt;/p&gt;

&lt;p&gt;That was the week I stopped thinking about upgrades as a release-engineering problem, and started thinking about them as the hardest product decision a low-code platform makes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your release note is somebody's Tuesday morning
&lt;/h2&gt;

&lt;p&gt;When you ship a normal SaaS product, an upgrade is a deployment detail. The code is yours, the tests are yours, and the blast radius is measured in your own repository.&lt;/p&gt;

&lt;p&gt;A low-code platform is different in one specific and unforgiving way: your users are also your developers. Their applications are not source code you can refactor on a branch. They are metadata — tables, fields, validation rules, workflow steps, permission trees — assembled by people who will never read your changelog and did not ask to participate in your roadmap.&lt;/p&gt;

&lt;p&gt;Which means every release you ship is deployed to every application anyone has ever built on your platform. Including the one a contractor assembled in 2022 and abandoned. Including the one whose only purpose is that a single department needed a spreadsheet with approvals and an audit trail. Including the one that quietly became load-bearing infrastructure for a factory floor, while its author moved to a different team and forgot it existed.&lt;/p&gt;

&lt;p&gt;Most teams measure a change by its blast radius in the codebase. In a low-code platform, the blast radius is somebody's business process — and you do not have a test suite for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata is data. That is the entire problem.
&lt;/h2&gt;

&lt;p&gt;Here is the sentence I keep repeating to new engineers on the team, and the one that took me the longest to internalize: &lt;strong&gt;the application model is data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "configuration." Not "the customer's workspace." Data. Rows in tables, written by users, referenced by other rows, with all the mess that implies.&lt;/p&gt;

&lt;p&gt;Once you accept that, the consequence is immediate and uncomfortable. Every change you make to the model is a data migration — on other people's data, in production, typically without a rollback path, and with a few thousand people downstream who find out by watching their app behave differently on Monday.&lt;/p&gt;

&lt;p&gt;A schema change you would happily make in a codebase becomes a decision with a completely different cost structure. Rename a field in your own codebase: the compiler tells you every place that broke. Rename a field on someone else's application: nothing tells you anything, and the workflow that referenced it fails silently at 2 a.m. on a Saturday.&lt;/p&gt;

&lt;p&gt;The version number on your release is not the version number that matters. What matters is the version of every application built on top of you, and you did not write those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deprecation is a promise you make to strangers
&lt;/h2&gt;

&lt;p&gt;The warehouse incident turned out to be a lookup field that we had quietly improved. The new behavior was better in every case we had tested.&lt;/p&gt;

&lt;p&gt;The customer's app had been written against the old behavior, deliberately or not, and had been correct for eight months. Our release notes were honest. Our tests were green. And a person who has never met us inherited a broken app on a Thursday.&lt;/p&gt;

&lt;p&gt;That is what deprecation actually is. Not a line in a changelog. A promise you make to a stranger you will never meet, about a system they built and you will change.&lt;/p&gt;

&lt;p&gt;I used to think of backward compatibility as a box we ticked. Now I think of it as the primary surface of the product. The builder is the exciting part; the compatibility contract is the part that decides whether anyone dares to build anything serious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three upgrade strategies, all expensive
&lt;/h2&gt;

&lt;p&gt;Every platform team eventually runs this experiment, and every one of the three answers costs something real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship everything, all at once.&lt;/strong&gt; Fast for you, brutal for them. It works until it doesn't, and when it doesn't, you have broken a customer's operations with a fix you cannot un-ship. The velocity you gain is borrowed against trust you will need later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flag day: let the customer choose when.&lt;/strong&gt; Feels respectful, and it is. But it means you support the old behavior for as long as the slowest customer delays — and the customers who delay longest are, almost by definition, the ones whose applications are the most fragile and least understood. You have just guaranteed that your least maintained code paths serve your most at-risk users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compatibility forever.&lt;/strong&gt; Never remove anything. This is the most seductive option and the most corrosive. Within three years you are maintaining five subtly different behaviors for the same conceptual feature, your documentation describes a platform that no longer exists, and every new engineer has to learn all of it before they can change anything safely.&lt;/p&gt;

&lt;p&gt;We landed on a mix, and I would not pretend it is elegant: additive changes go out by default, behavioral changes get an announced window, and there is a short, explicit list of things we will simply not change without a customer-level conversation. The list is the important part. It is our promise written down, and it constrains us far more than it constrains them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed after that Thursday
&lt;/h2&gt;

&lt;p&gt;None of this is clever. All of it is the difference between a platform that customers trust with their core processes and one they merely tolerate.&lt;/p&gt;

&lt;p&gt;We started treating the application model as a versioned artifact. Before any release, we can diff a customer's workspace before and after, and see what would move. A behavioral change now gets run against real customer applications first, as a dry run, not just against our fixtures.&lt;/p&gt;

&lt;p&gt;We stopped letting destructive actions be silent. When a field is deleted or renamed, the platform shows the workflow steps, rules, and reports that referenced it — while the person doing the deletion still has the tool in their hand and the context in their head.&lt;/p&gt;

&lt;p&gt;We built the upgrade path into the product itself. Customers can see what changed and what it means to them, in their language, in their workspace. A blog post is not a migration plan.&lt;/p&gt;

&lt;p&gt;And we changed who we write release notes for. Not the person who built the app. The person who inherited it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;Every low-code platform is organized, from its first commit, around making things easy to &lt;em&gt;build&lt;/em&gt;. Almost none are organized around making things easy to &lt;em&gt;inherit&lt;/em&gt;. Building is where the demo lives, where the funding is, where the conference talk comes from.&lt;/p&gt;

&lt;p&gt;But here is what I resisted for a long time: you are not shipping software to a customer. You are the co-author of every application they ever built, permanently, whether or not you ever saw it. The upgrade path is not a maintenance concern bolted onto the product. The upgrade path &lt;em&gt;is&lt;/em&gt; the product, and it is the only part of your platform that every one of your customers will experience, on a Tuesday, without warning.&lt;/p&gt;

&lt;p&gt;We spent three years making the builder fast. The week the warehouse app broke was the week I understood that its maintainer — tired, new, and holding an application they did not build — was the customer we had been serving all along.&lt;/p&gt;

&lt;p&gt;Design the upgrade like it is the feature. Because for the person who inherits the app, it is the only feature they will ever notice.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>architecture</category>
      <category>devops</category>
      <category>enterprise</category>
    </item>
    <item>
      <title>The Expression Engine Is Small. That Is Exactly the Problem.</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Sun, 27 Sep 2026 01:46:15 +0000</pubDate>
      <link>https://dev.to/informat/the-expression-engine-is-small-that-is-exactly-the-problem-g14</link>
      <guid>https://dev.to/informat/the-expression-engine-is-small-that-is-exactly-the-problem-g14</guid>
      <description>&lt;p&gt;In June, a distributor customer of ours called about a quote that should never have existed. Their quoting app has a validation rule: the discount field must stay under thirty percent unless the record carries an approval flag. The rule had been there for a year. It was written by their finance lead herself, in the platform's formula editor, in about four minutes.&lt;/p&gt;

&lt;p&gt;The quote in question had a discount of eighty percent. It had been created by a bulk import from their old system, during a product-line migration. The validation formula had evaluated exactly zero times on its journey into the database. Not because the rule was broken — because the rule only ran where the form ran, and an import is not a form.&lt;/p&gt;

&lt;p&gt;Nobody had ever told the finance lead that. Nobody had ever told &lt;em&gt;me&lt;/em&gt; that, in those words, and I build the thing.&lt;/p&gt;

&lt;p&gt;That phone call rearranged how I think about a component I had mentally filed under "small": the expression engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is not one feature. It is six wearing a trench coat
&lt;/h2&gt;

&lt;p&gt;When you say "formula" in a low-code platform, most people picture the computed field. The little column that multiplies quantity by price. That is the least interesting instance.&lt;/p&gt;

&lt;p&gt;Look at where expressions actually live in a platform like ours. Default values on fields. Validation rules. Visibility conditions on form sections. Branch conditions in workflows. Filters in reports and list views. Threshold checks in automations. Six subsystems, and in the early version of our platform, six different ways of parsing what was supposed to be the same language.&lt;/p&gt;

&lt;p&gt;That last sentence should horrify you.&lt;/p&gt;

&lt;p&gt;Because here is the thing about an expression engine: it is not a feature. It is a &lt;em&gt;language&lt;/em&gt;, and a language that appears in six places is six chances to disagree with itself. The day a user discovers that &lt;code&gt;empty&lt;/code&gt; means one thing in a validation rule and something subtly different in a workflow branch is the day they stop trusting every formula they read.&lt;/p&gt;

&lt;p&gt;Unifying the parsers was tedious work with no visible payoff. It was also one of the highest-leverage things we have done: a user who learned the language once could finally predict the platform everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every formula is a tiny contract with a field that can disappear
&lt;/h2&gt;

&lt;p&gt;The second thing the discount incident taught me is that a formula is not text. It is a &lt;em&gt;dependency&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When the finance lead wrote her validation rule, she referenced a field called Approved. That reference is a promise: as long as this formula exists, the field it points at exists, and the platform knows the formula must re-evaluate when the field changes. Multiply that promise by a few thousand formulas across a customer's workspace and you have a dependency graph that nobody drew and everybody relies on.&lt;/p&gt;

&lt;p&gt;Now let a user delete or rename that field. What happens to the formula?&lt;/p&gt;

&lt;p&gt;The lazy answer is that the formula keeps the old text and breaks at runtime. A rule that looks fine in the editor and fails silently in production is the worst failure mode there is: no exception, just a quiet wrong answer, invisible in the place people check and wrong in the place they don't.&lt;/p&gt;

&lt;p&gt;The expensive answer is a real dependency graph. Deleting a field tells you what references it. Renaming a field updates every formula atomically. Formula edits are validated against the current schema at save time, not at evaluation time. And when a formula cannot be fixed automatically, the platform says so loudly, at the moment of the destructive action, while the person who can fix it is still holding the tool.&lt;/p&gt;

&lt;p&gt;None of this shows up on a feature list. All of it shows up in whether customers dare to evolve their apps after six months of use. A platform where users are afraid to rename fields is a platform that has quietly taught its users to stop building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Null is the real language
&lt;/h2&gt;

&lt;p&gt;Here is the part of expression engine design that consumed the most arguments per line of spec: what happens when a value is missing.&lt;/p&gt;

&lt;p&gt;Every business table is full of holes: a quote without a close date, an order where the discount field was never touched, a contact imported without a phone number. Formulas run into those holes all day long.&lt;/p&gt;

&lt;p&gt;So the questions arrive immediately. Is an empty text field null, or an empty string? Is an untouched number null, or zero? And crucially: does a validation rule that evaluates to &lt;em&gt;null&lt;/em&gt; pass or fail? We decided, after real deliberation, that a rule which cannot produce an answer should fail closed. A rule that says "this must be under thirty percent" cannot vouch for a number it cannot see. Our finance lead, when I explained this a month after the incident, said: "Obviously." It was not obvious. We chose it. She just happened to agree.&lt;/p&gt;

&lt;p&gt;Type coercion is the same story. A formula language that silently turns "10" into 10 is friendly for ten minutes and a liability for a decade, because the string that looks numeric is not always numeric. We chose to be strict and provide explicit conversion functions, and yes, users complained about the strictness for a quarter. Then they stopped filing the bugs that come from silent coercion.&lt;/p&gt;

&lt;p&gt;Dates deserve their own confession. We still carry scar tissue from formulas that computed "days overdue" differently depending on which server evaluated them, before we decided the language would treat a zoned instant and a plain calendar date as &lt;em&gt;different types&lt;/em&gt;, not one type with a surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a formula runs matters more than what it says
&lt;/h2&gt;

&lt;p&gt;The discount formula said exactly the right thing. It ran in the wrong place.&lt;/p&gt;

&lt;p&gt;The deep design question is not the grammar. It is the execution contract: which expressions run in the browser as the user types, and which run on the server in the write path.&lt;/p&gt;

&lt;p&gt;Visibility conditions want to be instant, so they run client-side. Validation, computed fields, and workflow branches &lt;em&gt;must&lt;/em&gt; run server-side, because the write path is the only place where every door leads through the same checkpoint. Forms, imports, API calls, automations — if the rule lives there, a discount of eighty percent cannot exist, no matter which door it came through.&lt;/p&gt;

&lt;p&gt;This sounds like an implementation detail. It is a philosophical one. A low-code platform makes a promise to every builder: the constraints you express are properties of the &lt;em&gt;data&lt;/em&gt;, not of one particular screen. The moment a rule is a property of a screen, you have shipped a suggestion, not a constraint. Suggestions do not survive contact with integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The security line nobody sees
&lt;/h2&gt;

&lt;p&gt;One more uncomfortable thing. An expression engine is one step away from being a scripting engine, and the distance is measured in "one more useful function." Users will ask for string manipulation, then lookups, then loops, then "just a small HTTP call." Each request is reasonable. The accumulation is a scripting engine with no review, no permissions, and no audit, running inside every form on the platform.&lt;/p&gt;

&lt;p&gt;We drew the line deliberately: expressions compute values over the record they are attached to. They get a whitelist of pure functions. Data from other tables arrives through explicit, permission-checked references — the same permission system that governs the UI, not a side door. The moment someone needs more, the answer is the scripting layer, with its own governance. Keeping that line bright is a permanent negotiation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;Here is what I resisted for years: a low-code platform's expression engine is its second programming language, and for most users of the platform, it is the &lt;em&gt;first&lt;/em&gt; one. More people will write formulas than will ever open the scripting editor. Its semantics — what null means, what a failed validation means, what a rename does — will outlive your API versioning, your theming, and half your features.&lt;/p&gt;

&lt;p&gt;We did not design it like a language. We designed it like a convenience, and the discount incident was the language reading its own terms back to us.&lt;/p&gt;

&lt;p&gt;The uncomfortable version is this: the smallest component in your platform is the one your users live in daily. They will never see the workflow engine's state machine or the query planner's choices. They will see your null handling, every afternoon, in every table.&lt;/p&gt;

&lt;p&gt;Design it like a language. Or one June, a phone call will design it for you.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>architecture</category>
      <category>programming</category>
      <category>enterprise</category>
    </item>
    <item>
      <title>Can Your Low-Code Platform Survive a Second Language?</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Sat, 26 Sep 2026 01:36:55 +0000</pubDate>
      <link>https://dev.to/informat/can-your-low-code-platform-survive-a-second-language-4ldd</link>
      <guid>https://dev.to/informat/can-your-low-code-platform-survive-a-second-language-4ldd</guid>
      <description>&lt;p&gt;In March this year, a customer of ours who runs a mid-sized manufacturing group called me about something that had nothing to do with features. They had spent two years building an ERP on our platform. Purchase orders, quality inspection, warehouse management, the works. It worked. Their people knew it. Then they acquired a factory overseas, and the new general manager there asked, very politely, one question during the handover meeting: "Can the system speak English?"&lt;/p&gt;

&lt;p&gt;I said yes. Of course I said yes. And then I spent the next two months learning exactly how much that "yes" was going to cost.&lt;/p&gt;

&lt;p&gt;Internationalization sounds like a checkbox. Add a language switcher, translate some labels, done. Every platform marketing page lists "multi-language support" in the same sentence as "multi-tenant" and "responsive layout." It sits there, harmless, one bullet among many.&lt;/p&gt;

&lt;p&gt;It is not a checkbox. It is an architectural confession. A second language does not test whether you can translate strings. It tests where language actually lives inside your platform — and in most low-code platforms, the honest answer is: everywhere, carelessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thing that breaks is the schema, not the UI
&lt;/h2&gt;

&lt;p&gt;When people think of i18n, they think of interface text. Buttons, menus, placeholders. That part is genuinely easy. Any competent platform has a translation layer for its own chrome.&lt;/p&gt;

&lt;p&gt;But a low-code platform does not just ship a UI. It ships a &lt;em&gt;construction kit&lt;/em&gt;. The customer builds their own data models, their own forms, their own workflows, their own validation rules. So the question is not "can our menus speak English." The question is: when a customer renames a field, where does that name go?&lt;/p&gt;

&lt;p&gt;If the field label is a single string on the field definition, you have a problem. That string is simultaneously the display label, the thing the search engine indexes, the thing reports print in headers, and the thing AI-generated descriptions reference. When the second language arrives, you discover that "label" was never one thing. It was four things wearing one coat.&lt;/p&gt;

&lt;p&gt;The fix is not adding a second string. It is deciding that text visible to humans is a &lt;em&gt;localized object&lt;/em&gt; — a small map from locale to string, with a fallback chain, from the very first day you design the metadata model. Retrofitting that after a thousand tenants have built apps is one of the least pleasant exercises in software engineering. I have done adjacent versions of it. I do not recommend it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dropdowns are where optimism goes to die
&lt;/h2&gt;

&lt;p&gt;Here is the detail that humbled me. A form field of type "single select" has options. Each option has a display name. In the customer's original build, those option names were ordinary text: "Pending," "Approved," "Rejected" — except in Chinese, obviously.&lt;/p&gt;

&lt;p&gt;Now add English. Where does "Approved" live? If options are strings, someone has to duplicate every option list per language, and now the data stored is the Chinese string, so filtering "status equals Approved" silently returns nothing in the English UI. If you change the option to store an ID instead, you have migrated every workflow condition, every report filter, every script that compares against the old literal.&lt;/p&gt;

&lt;p&gt;We watched a variant of this happen in the customer's approval workflow. The workflow engine stored node names and branch conditions as text. The branch said: if department equals "采购部", route to procurement lead. In English UI, the department dropdown showed "Procurement." The condition never matched. Orders quietly fell into the default branch. Nothing errored. It just made wrong decisions, politely, for eleven days before anyone noticed.&lt;/p&gt;

&lt;p&gt;That is the real danger of i18n failures in low-code platforms. They do not throw exceptions. They translate the surface and leave the logic monolingual, and the system keeps running with a small, invisible misunderstanding at its core.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data itself refuses to be translated
&lt;/h2&gt;

&lt;p&gt;So you localize the schema. Good. Now the harder truth: the &lt;em&gt;data&lt;/em&gt; in the tables is whatever users typed. A product name entered as text by the Shanghai office is Chinese. The German salesperson opens the same record and sees Chinese. You can localize every label on the screen and the content is still in one language.&lt;/p&gt;

&lt;p&gt;Some of this you solve with structure — translation tables for reference data like categories, units, currencies. Some of it you honestly cannot solve, and the platform's job is to say so: separate what is &lt;em&gt;system text&lt;/em&gt; (translatable), what is &lt;em&gt;master data&lt;/em&gt; (translatable with discipline), and what is &lt;em&gt;user content&lt;/em&gt; (not translatable, and pretending otherwise creates a maintenance nightmare).&lt;/p&gt;

&lt;p&gt;And then there is the layer nobody budgets for: formats. Dates, numbers, names. Does your date field store a timezone or not? Does "03/04" mean March 4th or April 3rd depending on who is looking? Is a customer name one string, or a family name and a given name — because the Japanese and Icelandic users will fight you on that. These sound like trivia until they are payroll amounts or delivery dates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a platform owes its builders
&lt;/h2&gt;

&lt;p&gt;The uncomfortable part of this work was realizing that internationalization is not a feature you add to a low-code platform. It is a &lt;em&gt;contract&lt;/em&gt; you make with everyone who builds on it.&lt;/p&gt;

&lt;p&gt;The contract says: every piece of human-readable text you author — field labels, option names, workflow node names, validation messages, email templates, dashboard titles, automation notifications — is locale-aware from the moment you type it. You never see the machinery, but it is there. If we ship that contract late, every app built before it is silently in debt, and we cannot pay that debt for the customer. They have to pay it, record by record, option by option.&lt;/p&gt;

&lt;p&gt;This is also where low-code platforms differ from frameworks. A web framework hands i18n to the developer, who can adopt it per project. A platform like ours owns the metadata model for &lt;em&gt;every&lt;/em&gt; project ever built. We do not get to opt in per app. The architecture decision is platform-wide, permanent, and its cost lands on customers who trusted us.&lt;/p&gt;

&lt;p&gt;There is a second-order effect I did not anticipate: workflows and automations. A scheduled notification that says "您的订单已审批" is generated by a workflow definition. The report header that prints the wrong quarter because it formats dates with a hardcoded pattern. The AI agent that answers in Chinese because its prompt template was written in Chinese. Every subsystem that generates human-readable output is a translation surface, and each one you forget becomes a support ticket from a very polite manager in another country.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;I used to believe internationalization was a late-stage concern. Build the product first, prove the value, globalize later. That logic feels pragmatic. It is how almost every enterprise software company actually behaves.&lt;/p&gt;

&lt;p&gt;But here is what the two months taught me: you cannot retrofit "where language lives." You can retrofit translations. You cannot retrofit the decision that a field label is a map instead of a string, that an option stores an ID, that a workflow condition never compares display text. Those are not translations. They are the shape of your metadata, and the shape was poured in concrete on day one.&lt;/p&gt;

&lt;p&gt;The customer got their English interface. It works. It cost us far more than it would have cost as a design constraint nobody could feel at the time — because on day one, in one language, multi-language support looks like pure overhead with zero visible benefit.&lt;/p&gt;

&lt;p&gt;That is the trap, and it generalizes. The architectural decisions that matter most are exactly the ones whose cost is invisible when you skip them and expensive when you discover them. A second language is just the most polite messenger. It does not argue. It waits until you have a customer abroad, and then it reads your metadata model out loud.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>i18n</category>
      <category>architecture</category>
      <category>enterprise</category>
    </item>
    <item>
      <title>Who Changed This Record? The Question Your Low-Code Platform Must Answer</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Fri, 25 Sep 2026 02:31:14 +0000</pubDate>
      <link>https://dev.to/informat/who-changed-this-record-the-question-your-low-code-platform-must-answer-25eg</link>
      <guid>https://dev.to/informat/who-changed-this-record-the-question-your-low-code-platform-must-answer-25eg</guid>
      <description>&lt;p&gt;Last spring, I sat on a review call with a customer who had been running their entire HR operation on our platform for two years. Nothing was broken. No incident, no outage, no data loss. The HR director asked one question, almost apologetically: "An employee says her leave balance was wrong in March. Can you show me who changed the approval rule, and when?"&lt;/p&gt;

&lt;p&gt;I could not. Not in any form she could read, and not in any form an auditor would accept.&lt;/p&gt;

&lt;p&gt;We had logs. We had plenty of logs. What we did not have was an answer.&lt;/p&gt;

&lt;p&gt;That call rearranged my understanding of what an enterprise low-code platform actually owes its buyers. I had spent years treating audit capability as a checkbox — a nice-to-have that enterprise procurement departments mention in passing. I was wrong. It is closer to the foundation than the workflow engine, because without it, the workflow engine is a machine nobody can trust after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  We Had Logs. We Did Not Have an Answer.
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable detail: our platform did record changes. Every rule edit, every permission tweak, every published configuration change landed somewhere in our infrastructure. If you gave me database access and twenty minutes, I could reconstruct what happened to that leave rule in March.&lt;/p&gt;

&lt;p&gt;But the HR director did not need a reconstruction. She needed a record.&lt;/p&gt;

&lt;p&gt;The difference matters more than it sounds. A reconstruction is something an engineer does, under time pressure, with privileged access, producing a conclusion nobody else can verify. A record is something the platform maintains continuously, in the language of the business, that any authorized person can read on their own. The first is a favor. The second is a system.&lt;/p&gt;

&lt;p&gt;Most low-code platforms, including mine for a long time, ship the favor and call it an audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Audit Trail Is Not a Debug Log
&lt;/h2&gt;

&lt;p&gt;When I finally sat down to design this properly, the first mistake I made was treating it as a logging problem. It is not. A debug log answers "what did the system do?" An audit trail answers "who did what, to which business object, when, and from where?" Those overlap less than you would think.&lt;/p&gt;

&lt;p&gt;The events that matter in a low-code platform are not error events. They are quiet, ordinary, business-shaped events. Someone lowered an approval threshold. Someone added themselves to a role. Someone changed a field from optional to required, or edited a validation formula, or published a new version of a form that three hundred employees use daily.&lt;/p&gt;

&lt;p&gt;Notice that a large portion of these are build-time events, not run-time events. This is where low-code differs sharply from traditional software. In a conventional application, the people who change behavior are engineers, and their changes travel through version control with commits, reviews, and blame. In a low-code platform, the change-makers are HR admins, operations managers, finance staff — people who will never open a git repository. The platform is their version control, whether it wants to be or not.&lt;/p&gt;

&lt;p&gt;If the platform does not keep a readable history of who changed what, it has quietly handed every customer the operational risk of production changes with none of the safety nets engineers take for granted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Parts Nobody Puts in the Demo
&lt;/h2&gt;

&lt;p&gt;Designing the audit trail was humbling, because every part of it was harder than the slide in my head.&lt;/p&gt;

&lt;p&gt;The first hard part is deciding what deserves to be recorded. Record too little and the trail is useless exactly when it matters. Record everything and you drown the reader — a form save can touch a dozen objects, and a busy tenant generates thousands of events a day. We settled on a principle: record every event that changes meaning, not every event that changes bytes. A rename of a field label matters. An internal cache rebuild does not.&lt;/p&gt;

&lt;p&gt;The second hard part is readability. The first version of our audit log recorded raw identifiers: user IDs, object IDs, internal field names. Technically complete, practically useless. "User 8842 modified Field f_193 on Object obj_77" answers nothing. The record has to be written in the vocabulary of the customer: names, form titles, before-and-after values. That means the audit layer must resolve references at write time, because the objects it points to will themselves change or disappear later.&lt;/p&gt;

&lt;p&gt;The third hard part is immutability, or at least the credible appearance of it. An audit log that a tenant administrator can edit is not an audit log; it is a diary. Ours is append-only, with restricted deletion, and — this part took real argument internally — the audit log itself is subject to permissions. Not everyone who can view the HR app can view who changed the HR app. Treating the audit system as its own permission domain felt like over-engineering until the first customer asked whether their department managers could see records about salary formulas. They could not, and should not.&lt;/p&gt;

&lt;p&gt;The fourth hard part is retention and export. Enterprises do not want their audit history living forever inside your SaaS with no exit. They want to pull it into their SIEM, their compliance archive, their legal hold process. If your audit trail cannot leave your platform in a structured format, it is a view, not a record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability Is Not Just for Operations
&lt;/h2&gt;

&lt;p&gt;Somewhere in the middle of this work, I realized I had been thinking about observability too narrowly. I had borrowed the concept from operations — metrics, traces, logs for the running system — and assumed that was the whole story.&lt;/p&gt;

&lt;p&gt;But in a low-code platform, the "system" that misbehaves is often not the code. It is the configuration. The misbehavior is a permission added in April that quietly widened access, a workflow branch edited in June that started skipping a review step, an integration rewritten by someone who left the company in August. None of that shows up on a metrics dashboard. All of it shows up in a well-built audit trail.&lt;/p&gt;

&lt;p&gt;So for a platform like ours, the audit trail is not a compliance accessory next to the observability stack. It is the observability stack — for the layer where most of the real risk lives, which is the layer the customers themselves control.&lt;/p&gt;

&lt;p&gt;There is a second-order benefit I did not anticipate: the audit trail changed how customers use the platform. Once admins knew every change was attributed and visible, they started coordinating differently. Fewer silent edits. More use of draft-and-publish, because a publish event was now a visible, named act. Visibility did not just record behavior; it shaped it. That, I think, is the deepest argument for building this well — an audit trail is not just evidence after an incident, it is a discipline built into daily work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;The HR director's question cost us a quarter of engineering work to answer properly, and I have thought about why it took an incident to surface it.&lt;/p&gt;

&lt;p&gt;The honest answer is that audit capability has no champion during design. Nobody demos it. It wins no sign-ups, appears in no launch post, and the users who will depend on it do not know they need it until the day they desperately do. Every incentive in a competitive platform market pushes audit work to the bottom of the backlog. Ours sat there for years.&lt;/p&gt;

&lt;p&gt;The less comfortable answer is that I silently assumed our customers would always have engineers available to reconstruct history when needed. That assumption is not just wrong for low-code — it is the exact opposite of what low-code promises. We sell the idea that business people can safely own their systems. You cannot sell that and simultaneously sell them a platform where ownership without a memory.&lt;/p&gt;

&lt;p&gt;A platform that lets anyone change anything, and remembers nothing, is not empowering. It is an unlit warehouse.&lt;/p&gt;

&lt;p&gt;Build the lights in before the first tenant moves in.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>observability</category>
      <category>security</category>
      <category>enterprise</category>
    </item>
    <item>
      <title>Multi-Tenancy Is Not a Deployment Model. It Is a Data Model Decision.</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Thu, 24 Sep 2026 02:08:06 +0000</pubDate>
      <link>https://dev.to/informat/multi-tenancy-is-not-a-deployment-model-it-is-a-data-model-decision-4fg</link>
      <guid>https://dev.to/informat/multi-tenancy-is-not-a-deployment-model-it-is-a-data-model-decision-4fg</guid>
      <description>&lt;p&gt;The meeting lasted eleven minutes. The IT director of a manufacturing group had one question: "We have four subsidiaries. Can they all run on this, with each one only seeing its own data?"&lt;/p&gt;

&lt;p&gt;Everyone in the room heard a configuration request. It was an architecture request, and we spent the next year learning the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three different things people call "tenant"
&lt;/h2&gt;

&lt;p&gt;The word hides at least three separate concerns, and the trouble starts when a customer uses it to mean all of them at once.&lt;/p&gt;

&lt;p&gt;There is a deployment tenant: a separate installation, a separate database, a separate upgrade. There is a data tenant: one installation, many logical partitions, one shared schema. And there is a configuration tenant: the same data boundary, but a different set of forms, workflows, and permission rules.&lt;/p&gt;

&lt;p&gt;Most platforms pick one of these and quietly assume the other two come along for free. Our customer wanted all three, for four subsidiaries, in one deployment, sharing one application template, with the ability to change that template for one subsidiary without touching the others.&lt;/p&gt;

&lt;p&gt;That last clause is where a low-code platform stops behaving like a normal SaaS product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata is data too
&lt;/h2&gt;

&lt;p&gt;In a conventional multi-tenant application, isolation is a filter. Every query carries a tenant column, every index starts with it, and the code review question is refreshingly boring: did you remember the where clause?&lt;/p&gt;

&lt;p&gt;A low-code platform does not have that luxury, because the thing being isolated is not only the customer's business records. It is the definition of the application itself: the fields, the views, the permission rules, the workflow versions, the automation triggers, the scripts, the dashboards. All of that is rows in tables too, and all of it is tenant-scoped.&lt;/p&gt;

&lt;p&gt;So the question multiplies. When a user in subsidiary A saves a list view, does that saved filter belong to them, to their subsidiary, or to the template everyone shares? When subsidiary B's admin fixes a bug in a shared workflow, do the other three get the fix, or an unexpected change on a Tuesday afternoon?&lt;/p&gt;

&lt;p&gt;There is no universally correct answer. There is only an answer the customer can predict. That is the real deliverable, and it is much harder to produce than a where clause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customization is where the boundary leaks
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable shape of the problem. The reason people buy a low-code platform is customization. The reason multi-tenancy is hard is customization. These are the same property.&lt;/p&gt;

&lt;p&gt;Every customization surface is a candidate isolation hole. Custom fields: whose field is it, and what happens when a shared report assumes it exists? Custom scripts: this is the worst one, because it is user-authored code running inside the platform's own process, with the platform's own privileges. A script written by subsidiary A that quietly reads "all orders" is not a bug in the script. It is a bug in the boundary, and the person who wrote it had no idea they were crossing a line, because from where they sat there was no line to see.&lt;/p&gt;

&lt;p&gt;We learned to think of it this way: the tenant boundary cannot be enforced by the application builder, because the application builder is a tenant. It has to be enforced by the runtime, at every point where metadata meets data, whether or not the caller is aware there is a boundary at all.&lt;/p&gt;

&lt;p&gt;That means the enforcement point is not one place. It is the query compiler, the script sandbox, the workflow engine, the automation scheduler, the file store, the export pipeline, and now the AI agent. Every one of those has to know who is asking, and none of them can be trusted to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tenant that ruins it for everyone
&lt;/h2&gt;

&lt;p&gt;Then there is the failure mode nobody puts in the sales deck: one tenant making the product bad for everyone else.&lt;/p&gt;

&lt;p&gt;A shared runtime means shared connection pools, shared caches, shared background workers, shared rate limits. One subsidiary imports a two-million-row spreadsheet before lunch and the other three watch their list views crawl. One automation goes into a retry loop and eats the scheduler.&lt;/p&gt;

&lt;p&gt;In a single-tenant installation, this is a performance bug. In a multi-tenant one, it is a fairness bug, and fairness is much harder to reason about than throughput. You end up building per-tenant quotas, per-tenant queues, and a notion of priority that never appeared in any requirement, because the customer never asked "can we be slow?" They asked to share a system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaving is harder than joining
&lt;/h2&gt;

&lt;p&gt;The part nobody plans for is exit.&lt;/p&gt;

&lt;p&gt;When a tenant wants to leave — a contract ends, a subsidiary is sold, a regulation demands deletion — you have to answer a question the design has been avoiding for two years: what exactly belongs to them?&lt;/p&gt;

&lt;p&gt;Their business records, obviously. But also their attachments, their audit history, their script versions, their workflow instances, the reference data someone imported in year one, and possibly rows inside a shared table they can no longer distinguish from the shared rows, because the design treated "shared" and "theirs" as the same column.&lt;/p&gt;

&lt;p&gt;A platform that cannot delete a tenant cleanly cannot onboard one confidently. We found the honest test of a tenant boundary is not "can they see only their own data." It is "can we remove them completely, without touching anyone else, and prove it."&lt;/p&gt;

&lt;p&gt;If you cannot run that test, you do not have a tenant boundary. You have a convention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent does not know about tenants
&lt;/h2&gt;

&lt;p&gt;The newest pressure came from somewhere we did not expect: the AI agent.&lt;/p&gt;

&lt;p&gt;An agent that can query, create, and edit records is the most capable user the platform has ever had, and also the least aware of context. A human in subsidiary A has a session, a sidebar, and a mental model of where they are standing. A tool call has a scope parameter, and if that scope is missing, wrong, or inherited from the wrong place, the agent will answer a question about subsidiary B's revenue in exactly the same confident tone it uses for everything else.&lt;/p&gt;

&lt;p&gt;We had to stop treating the tenant as a part of the request and start treating it as part of the identity. The agent does not get to say who it is acting for; the runtime tells it, and the runtime refuses anything that would cross a line — including the seemingly innocent requests, like "summarize all orders in the system," which sounds reasonable until you remember that the system has four customers inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;We set out to build a platform where every customer feels like an owner. Multi-tenancy is the price of that feeling, and it is paid in the one currency we were least willing to spend: the assumption that the platform is only ever running for one person.&lt;/p&gt;

&lt;p&gt;The uncomfortable conclusion is that the feature we sell — deep, per-customer customization — is precisely the property that makes a tenant boundary unenforceable by policy. You cannot document your way out of it, because the document would have to be read by the person customizing the application, who is the one person who cannot see the line.&lt;/p&gt;

&lt;p&gt;The only boundary that holds is the one the platform enforces on itself, permanently, in every path where a definition becomes a query or an action. The customer gets to stop thinking about it.&lt;/p&gt;

&lt;p&gt;Which means, once again, we do not.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>saas</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>Your Low-Code Platform Is Fast Until a Customer Builds One Real Table</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Wed, 23 Sep 2026 02:11:43 +0000</pubDate>
      <link>https://dev.to/informat/your-low-code-platform-is-fast-until-a-customer-builds-one-real-table-2mlk</link>
      <guid>https://dev.to/informat/your-low-code-platform-is-fast-until-a-customer-builds-one-real-table-2mlk</guid>
      <description>&lt;p&gt;The demo table had 412 rows. It loaded in under a second. Everyone in the room nodded.&lt;/p&gt;

&lt;p&gt;Eight months later, that same customer's work order table had 4.1 million rows. The list view took fourteen seconds to draw its first page. The customer's operations manager opened a support ticket with one sentence: "The system is slow."&lt;/p&gt;

&lt;p&gt;The interesting part is not that a platform got slow. It is where it got slow, and why that place was inevitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The list view is where every abstraction converges
&lt;/h2&gt;

&lt;p&gt;A form is one record. A workflow is one instance. A dashboard is an aggregate someone designed to be cheap.&lt;/p&gt;

&lt;p&gt;A list view is different. It is the one screen where everything the platform promised has to be resolved at the same time: the field definitions, the relationships between tables, the permission rules, the workflow status, the computed columns, the saved filters, the sort order, and the user's own idea of what "the important records" are.&lt;/p&gt;

&lt;p&gt;In a demo, all of those layers are thin. Twelve fields, one related table, two hundred rows, one user with full access, no saved filters, default sort.&lt;/p&gt;

&lt;p&gt;In production, the same screen has eighty fields across four related tables, a row-level permission rule that depends on the user's department, and a saved filter someone created two years ago that nobody has looked at since.&lt;/p&gt;

&lt;p&gt;The design did not change. The definition of "fast" did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four walls, in the order you hit them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Filter translation.&lt;/strong&gt; In the interface, a filter says "Amount is greater than 10000." In the database, it says nothing, because the platform does not own physical columns — it owns metadata. It has to compile user intent into a query. That compilation is where the first bug lives, because a field's type is not fixed. The same logical field can be numeric in one application and text in another, and the string comparison and the numeric comparison return different records. A filter that silently returns the wrong rows is worse than a filter that is slow, because slow gets reported and wrong does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permission-aware filtering.&lt;/strong&gt; This is the one that breaks the performance model permanently. In a low-code platform, permissions are not a wrapper around the query. They are part of the query. If a user can only see records in their own department, then every count, every page, every aggregate, and every export must carry that restriction.&lt;/p&gt;

&lt;p&gt;The tempting shortcut is to fetch and then filter in application code. It works beautifully at four hundred rows and catastrophically at four million, and it introduces a quieter failure: the pagination gets confused. Total count says two thousand, the user sees sixty records per page, and page nine is empty. Then someone builds a report on the same numbers and the report disagrees with the screen, and now you are debugging a trust problem, not a latency problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related field display.&lt;/strong&gt; Showing "Customer Name" on a list of orders is a join. Most platforms hide this until the customer asks for their fifth related column, and then it becomes a join graph. The natural implementation is one query per row, which is elegant and dies at scale. The natural correction is one enormous query with twenty joins, which the planner may handle, until the day it does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sorting and computed columns.&lt;/strong&gt; Sorting by a stored, indexed column is cheap. Sorting by a computed column — a formula, a rollup from a child table, a workflow-derived status — means the database cannot use an index at all. It must evaluate the expression for every candidate row before it can decide what the first page is. At ten thousand rows nobody notices. At a million rows the sort is the query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that took me too long to accept
&lt;/h2&gt;

&lt;p&gt;Performance work in a low-code platform is not a database administration problem. It is a metadata problem.&lt;/p&gt;

&lt;p&gt;You cannot hand your slow queries to a DBA, because at the moment the customer complains, the query does not exist yet. It gets generated at runtime out of a form definition, a permission rule set, and a filter someone saved in 2024. There is no query to tune.&lt;/p&gt;

&lt;p&gt;So you end up building a small query planner. You end up with a cost model that knows the shape of the permission rule, because a permission filter that resolves to an indexed column is a different animal from one that resolves to a subquery over an access graph. You end up with a plan cache, because compiling metadata to a query is not free and doing it on every request is absurd. You end up with a concept of which view configurations are "materializable" and which must be computed live.&lt;/p&gt;

&lt;p&gt;Nobody puts "build a miniature database optimizer" on a low-code platform roadmap. It shows up anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moved the needle
&lt;/h2&gt;

&lt;p&gt;Four things, in order of impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop assembling the view at request time. Compile it.&lt;/strong&gt; The combination of view definition, permission shape, and filter set is a query plan, and it can be built once and cached. The list view became a compiled artifact rather than a runtime negotiation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the permission filter cheap.&lt;/strong&gt; Precompute the visible set, or denormalize the scope into a column that can be indexed. The rule should resolve to a seek, not a traversal. This single change took more time off the worst screens than any index we added.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admit that two engines exist.&lt;/strong&gt; An interactive list view and a data export have completely different jobs. One must return a first page in a few hundred milliseconds, because a human is watching. The other may legitimately take thirty seconds, because nobody is. Trying to run both through one code path is how you get a system that is slow at three hundred rows and times out at ten million.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop promising deep pagination.&lt;/strong&gt; Offset pagination into the millions is a promise no database keeps. Deep pages need a cursor based on the sort key, not an arithmetic skip.&lt;/p&gt;

&lt;p&gt;And one thing that is not performance work at all but changed how users felt about the platform: telling the truth in the interface. A message saying the filter matched an enormous number of records and should be narrowed is a better experience than a spinner, even when both take three seconds. Users forgive slow. They do not forgive unexplained slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent made it worse before it made it better
&lt;/h2&gt;

&lt;p&gt;We added the ability for an AI agent to query, create, edit, and delete records against these same tables. It is genuinely useful. It is also the most aggressive list view user you will ever have.&lt;/p&gt;

&lt;p&gt;A human scrolling a slow table gives up after two pages. An agent does not give up, and it does not understand that asking for "all matching records" on a table with four million rows is a different request from asking for the first twenty. Almost everything we learned about interactive performance had to be re-learned as a machine-facing contract: hard limits, mandatory cursors, and a refusal to answer unbounded questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;Low-code platforms are sold on the promise that you no longer need to think about databases. That promise is real for the customer, and it is a lie for us.&lt;/p&gt;

&lt;p&gt;Someone still has to think about indexes, cardinality, execution plans, write amplification, and permission scope acting as an access path. The only thing the platform changes is who: it cannot be the person building the application. It has to be the platform itself, permanently, on behalf of users who will never know there was a decision to make.&lt;/p&gt;

&lt;p&gt;Which means every abstraction we sell is a debt instrument. The form engine, the permission system, the workflow engine, the plugin mechanism — each one buys simplicity at the top of the stack and pays for it in complexity at the bottom. The list view is simply where the bill arrives, and it arrives in year two, when the platform is load-bearing, the original demo is forgotten, and nobody remembers that the first version was fast because the table was small.&lt;/p&gt;

&lt;p&gt;I used to think of performance as a phase. Something you do after the features work.&lt;/p&gt;

&lt;p&gt;It is not a phase. It is the cost of the abstraction, and it comes due every time a customer's business succeeds enough to produce real data.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>database</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Every Low-Code Platform Eventually Becomes an Integration Platform</title>
      <dc:creator>informat</dc:creator>
      <pubDate>Tue, 22 Sep 2026 13:49:52 +0000</pubDate>
      <link>https://dev.to/informat/every-low-code-platform-eventually-becomes-an-integration-platform-3no8</link>
      <guid>https://dev.to/informat/every-low-code-platform-eventually-becomes-an-integration-platform-3no8</guid>
      <description>&lt;p&gt;In my last post about release processes, I made a claim that felt like a conclusion: a low-code platform that cannot safely change is not enterprise software.&lt;/p&gt;

&lt;p&gt;This week I realized that claim was missing half the picture.&lt;/p&gt;

&lt;p&gt;A platform that cannot safely change will lose enterprise customers eventually. But a platform that cannot safely &lt;strong&gt;connect&lt;/strong&gt; will lose them immediately.&lt;/p&gt;

&lt;p&gt;Here is the pattern I have now seen enough times to call it a law: every low-code platform eventually becomes an integration platform. No exceptions. The only question is whether you admit it early or discover it in a panic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The conversation that repeats with every customer
&lt;/h2&gt;

&lt;p&gt;Month one with a new enterprise customer is wonderful. We build their data model, their forms, their approval workflows. Everything lives inside the platform. It is clean, fast, and entirely ours.&lt;/p&gt;

&lt;p&gt;Month two always starts with the same sentence:&lt;/p&gt;

&lt;p&gt;"This is great. Now can it talk to our existing system?"&lt;/p&gt;

&lt;p&gt;Their existing ERP. Their HR system. Their email server. Their legacy order database from 2011 that nobody wants to touch but everybody depends on.&lt;/p&gt;

&lt;p&gt;The first time, I thought this was one customer's unusual situation. The third time, I saw the shape of it. The fifth time, I stopped being surprised and started rebuilding the platform around it.&lt;/p&gt;

&lt;p&gt;Inside a low-code platform, everything is connected by design. A field on a form is bound to a column in a table. A workflow step reads a field. A dashboard aggregates a table. The connections are internal, typed, and always available.&lt;/p&gt;

&lt;p&gt;The moment a customer's data lives partly in your platform and partly somewhere else, all of that elegance meets the outside world. And the outside world does not have your field IDs, your permission model, or your transaction guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: point-to-point integrations
&lt;/h2&gt;

&lt;p&gt;The natural first response is to build each requested connection by hand.&lt;/p&gt;

&lt;p&gt;Customer needs order data in their ERP? Write a script that pushes orders to the ERP. Customer wants employee data from HR? Write another script that pulls from HR. Customer wants a notification in their messaging tool? One more script.&lt;/p&gt;

&lt;p&gt;This works. For a while.&lt;/p&gt;

&lt;p&gt;Then you count. Five external systems, each needing data from two or three others. That is not five integrations. That is a graph, and the edges multiply. Someone changes a field name in the HR system and three of your scripts break silently, and the only person who notices is the HR manager whose monthly report now has empty columns.&lt;/p&gt;

&lt;p&gt;The problem is not the number of scripts. It is that each script is a private agreement between two systems, with no shared vocabulary, no shared error handling, and no shared place to look when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What integration actually breaks down into
&lt;/h2&gt;

&lt;p&gt;Once I stopped counting integrations and started studying them, four distinct jobs appeared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inbound: getting data in.&lt;/strong&gt; An external system creates or updates records in the platform. This needs a real API surface — not an internal endpoint repurposed, but a public one with the platform's own permission system attached. If the API does not respect the same rules as the UI, you now have two security models and one of them is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outbound: getting data out.&lt;/strong&gt; The platform pushes changes to external systems. This is where two engineering problems hide that nobody warns you about.&lt;/p&gt;

&lt;p&gt;The first is retries. External systems fail. Networks fail. Your push happens at the exact moment their system restarts. Without automatic retry with backoff, that record is simply gone, and nobody knows.&lt;/p&gt;

&lt;p&gt;The second is idempotency. The retry succeeds — but the first attempt had actually landed. Now the customer has two orders in their ERP for one order in your platform. An integration layer without idempotency keys is a machine for manufacturing duplicates at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Events: letting the outside world subscribe.&lt;/strong&gt; This is the one I resisted longest.&lt;/p&gt;

&lt;p&gt;For a long time I thought events were an advanced feature. Then I watched a customer build a "sync job" that polled our API every five minutes, compared timestamps, and pulled anything new. It worked. It was also fragile, slow, and completely blind to deletions.&lt;/p&gt;

&lt;p&gt;The platform already knows when a record is created, updated, or deleted. It already knows when a workflow step completes. It knows these things at the moment they happen. Emitting an event — record created, workflow finished, status changed — costs almost nothing, and it turns every polling script into a subscription.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets: where the credentials live.&lt;/strong&gt; An integration needs an API key, a token, a password. Where does it go?&lt;/p&gt;

&lt;p&gt;Not in a script. Not in a form field. Not in a configuration value visible to anyone with designer access.&lt;/p&gt;

&lt;p&gt;I learned this one embarrassingly late. Credentials belong in a dedicated, encrypted, access-controlled store that the integration layer reads but no script or page can dump. The first time a customer's auditor asks "who can see the ERP credentials," the answer must be a role, not "whoever can open the settings page."&lt;/p&gt;

&lt;h2&gt;
  
  
  The quiet problem: knowing when it broke
&lt;/h2&gt;

&lt;p&gt;The hardest part of integration is not sending data. It is knowing that sending data failed.&lt;/p&gt;

&lt;p&gt;An internal workflow fails, and the approver sees it. A form breaks, and users complain immediately. The feedback loop is built in.&lt;/p&gt;

&lt;p&gt;An outbound sync fails at 2 a.m. on a Saturday, and everything looks fine in the platform. The dashboard shows clean numbers. The workflows run green. Meanwhile, three days of orders never reached the ERP, and the finance team finds out during month-end reconciliation.&lt;/p&gt;

&lt;p&gt;So the integration layer needs its own observability. Every connection needs a visible run history, an error state that surfaces in the platform instead of in someone's inbox a week later, and a dead-letter state for events that could not be delivered — with a replay button. Not logs buried in a server. A screen that an operations person can check in ten seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable conclusion
&lt;/h2&gt;

&lt;p&gt;I used to describe our platform as a place to build business systems. Tables, forms, workflows, dashboards. A self-contained world.&lt;/p&gt;

&lt;p&gt;That description was accurate and useless, because no enterprise lives in a self-contained world.&lt;/p&gt;

&lt;p&gt;The real promise of a low-code platform is not that it replaces the customer's existing systems. It usually cannot, and the customer does not actually want it to. The promise is that it becomes the fastest place to build the connective tissue between the systems they already have — with permissions, audit, and rollback included.&lt;/p&gt;

&lt;p&gt;Which means the integration layer is not a feature on the roadmap.&lt;/p&gt;

&lt;p&gt;It is the reason the platform gets adopted, and the reason it gets trusted. The platforms that treat it as a checklist item spend their second year doing archaeology on broken scripts. The ones that treat it as core architecture spend their second year onboarding customers who already have five systems and no patience for another island.&lt;/p&gt;

&lt;p&gt;I know which second year I want.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>integration</category>
      <category>architecture</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
