<?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: Nebosa Company</title>
    <description>The latest articles on DEV Community by Nebosa Company (@nebosa_company_0edc92e18b).</description>
    <link>https://dev.to/nebosa_company_0edc92e18b</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%2F4131673%2F99532ac9-af01-4627-9b9d-f7641e48a8d6.png</url>
      <title>DEV Community: Nebosa Company</title>
      <link>https://dev.to/nebosa_company_0edc92e18b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nebosa_company_0edc92e18b"/>
    <language>en</language>
    <item>
      <title>One tenant took the whole connection pool</title>
      <dc:creator>Nebosa Company</dc:creator>
      <pubDate>Tue, 20 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/nebosa_company_0edc92e18b/one-tenant-took-the-whole-connection-pool-bg1</link>
      <guid>https://dev.to/nebosa_company_0edc92e18b/one-tenant-took-the-whole-connection-pool-bg1</guid>
      <description>&lt;p&gt;Our tenant isolation was correct. Two tenants, separate realms, separate apps,&lt;br&gt;
RLS confirmed both ways — the victim's token returns &lt;code&gt;[]&lt;/code&gt; for the storm&lt;br&gt;
tenant's table, and a script asserts it on every run. Nobody could read anybody&lt;br&gt;
else's rows.&lt;/p&gt;

&lt;p&gt;Then we pointed a load storm at one of them and the other one fell over.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" alt="Appdor in 26 seconds: grid, approval workflow, kanban, dashboard" width="720" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The product tour, silent by design. &lt;a href="https://appdor.com/media/homepage/tour.webm" rel="noopener noreferrer"&gt;Full-resolution video&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What we measured
&lt;/h2&gt;

&lt;p&gt;Tenant A issued heavy 200-row reads at 60 concurrent. Tenant B issued the light&lt;br&gt;
100-row read a grid does when you open it. Same stack, same database, 2,000&lt;br&gt;
rows each.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;phase&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;th&gt;requests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;3ms&lt;/td&gt;
&lt;td&gt;31ms&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;under storm&lt;/td&gt;
&lt;td&gt;3,194ms&lt;/td&gt;
&lt;td&gt;10,007ms&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recovered&lt;/td&gt;
&lt;td&gt;3ms&lt;/td&gt;
&lt;td&gt;5ms&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tenant B degraded 322× against a published bound of 2×, and 10.5% of its&lt;br&gt;
requests were refused outright. The 10,007ms p99 is a timeout ceiling, not a&lt;br&gt;
measurement — we never found out how slow it really was.&lt;/p&gt;

&lt;p&gt;These are single-machine figures: Docker, the harness and the browser all on&lt;br&gt;
one box. That is not a production result and we are not going to present it as&lt;br&gt;
one. It is the right shape of measurement for the question — does one tenant's&lt;br&gt;
load reach another tenant — and the wrong shape for "how fast is Appdor".&lt;/p&gt;

&lt;p&gt;The part that made it obvious this was contention and not a bug: the storm&lt;br&gt;
tenant's own throughput collapsed to 12.7 req/s. The instance did not throttle&lt;br&gt;
one tenant in favour of another. It fell over for both. And recovery was&lt;br&gt;
immediate — 5ms p99 the moment the storm stopped.&lt;/p&gt;
&lt;h2&gt;
  
  
  The mechanisms existed and nothing called them
&lt;/h2&gt;

&lt;p&gt;This is the uncomfortable part. The fairness module was already in the tree.&lt;br&gt;
It had tests. The only function of it the running product ever reached was the&lt;br&gt;
one that publishes the promise to the capability catalogue.&lt;/p&gt;

&lt;p&gt;A module that is imported by nothing on the request path is documentation with&lt;br&gt;
a test suite. We had shipped the paperwork.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix was in-flight caps, not rate limits
&lt;/h2&gt;

&lt;p&gt;The controller that now sits in front of PostgREST caps &lt;strong&gt;concurrent in-flight&lt;br&gt;
work per tenant&lt;/strong&gt;. That distinction is the whole result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;victim p99 under storm&lt;/td&gt;
&lt;td&gt;10,007ms&lt;/td&gt;
&lt;td&gt;25ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;degradation&lt;/td&gt;
&lt;td&gt;322×&lt;/td&gt;
&lt;td&gt;well inside the 2× bound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;victim requests refused&lt;/td&gt;
&lt;td&gt;10.5%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "after" column is the 2026-09-07 re-run against the current gateway: 3,000&lt;br&gt;
storm requests at 60 concurrent, two live tenants with tokens minted for&lt;br&gt;
separate users. Baseline p99 28ms, under storm 25ms, recovered 15ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The defensible claim is "well inside the bound", not a specific ratio,&lt;/strong&gt; and&lt;br&gt;
that is deliberate. The same harness has returned 0.50×, 0.14×, 0.73× and 0.89×&lt;br&gt;
across runs of unchanged code, because the ratio is computed against whichever&lt;br&gt;
baseline the first phase happened to measure — and a cold stack measured 200ms&lt;br&gt;
p99 where a warm one measured 44ms. A sub-1.0 ratio is evidence the baseline is&lt;br&gt;
still warming, not evidence that a storm makes the victim faster. Quoting the&lt;br&gt;
prettiest of those numbers would be the same species of mistake as the one this&lt;br&gt;
post is about.&lt;/p&gt;

&lt;p&gt;The storm's request &lt;em&gt;rate&lt;/em&gt; was modest. Its concurrent count was the entire&lt;br&gt;
connection pool. A token bucket would not have fixed this, and we can show that&lt;br&gt;
rather than assert it: at the shipped 200/s limit the rate limiter absorbed&lt;br&gt;
2,308 refusals; raising it to 5,000/s moved every one of those to the queue and&lt;br&gt;
changed the outcome not at all — 85% shed either way. When you can move a&lt;br&gt;
mechanism's threshold by 25× and the result does not care, that mechanism is&lt;br&gt;
not the one doing the work.&lt;/p&gt;
&lt;h2&gt;
  
  
  A wider queue made it worse
&lt;/h2&gt;

&lt;p&gt;We tried a 200-deep per-tenant queue on the theory that shedding less is&lt;br&gt;
kinder. It converted refusals into waiting: the storm took 4.9 minutes instead&lt;br&gt;
of 7 seconds, and the victim was still at 2.41× baseline when the run ended.&lt;br&gt;
It failed the recovery check.&lt;/p&gt;

&lt;p&gt;Refusing a request in 1ms is a better answer than serving it in 10 seconds.&lt;br&gt;
Queues are how you turn a load problem into a latency problem and then lose&lt;br&gt;
track of it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What we still cannot claim
&lt;/h2&gt;

&lt;p&gt;The criterion this was measured against is &lt;strong&gt;still marked unmet in our own&lt;br&gt;
audit&lt;/strong&gt;, and that is the most useful sentence in this post.&lt;/p&gt;

&lt;p&gt;It asks for a sustained automation event storm &lt;em&gt;and&lt;/em&gt; a public API burst,&lt;br&gt;
simultaneously. The harness drives the burst only. So the measurement supports&lt;br&gt;
a narrower claim than the criterion makes, and the honest version is "we fixed&lt;br&gt;
the case we can drive" rather than "we meet the bar we set ourselves". The&lt;br&gt;
audit row says so in the same words; we would rather you read it from us.&lt;/p&gt;

&lt;p&gt;The other thing the ratio cannot tell you is &lt;em&gt;why&lt;/em&gt;. A failure here says the&lt;br&gt;
promise broke, not which mechanism broke it — that is what the per-tenant&lt;br&gt;
gauges are for. A fairness number is a smoke alarm, not a diagnosis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3vjgui9enbfnf71d6hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3vjgui9enbfnf71d6hx.png" alt="A dashboard in Appdor" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this is in a spreadsheet-database you run yourself
&lt;/h2&gt;

&lt;p&gt;Appdor runs as one container you start yourself, so "multi-tenant" is not an&lt;br&gt;
abstraction our SaaS operator worries about on your behalf — it is your&lt;br&gt;
Postgres, your pool, and your two departments. The isolation you get from RLS&lt;br&gt;
is about &lt;em&gt;reading&lt;/em&gt;. Nothing in RLS stops one tenant from consuming the resource&lt;br&gt;
every other tenant needs, and the failure looks like a slow app, not like a&lt;br&gt;
security incident, which is why it survives so long.&lt;/p&gt;

&lt;p&gt;The storm is a script in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run &lt;span class="nb"&gt;test&lt;/span&gt;:fairness
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fails loudly, in the direction of the number above, if the controller ever&lt;br&gt;
comes unwired again — which is the only reason we trust it now, having already&lt;br&gt;
been wrong once about a module that existed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://appdor.com/blog/13-one-tenant-took-the-pool" rel="noopener noreferrer"&gt;appdor.com&lt;/a&gt;. I build Appdor, so weigh accordingly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>performance</category>
      <category>saas</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building a no-code app builder out of small pure modules, and testing each one on its own</title>
      <dc:creator>Nebosa Company</dc:creator>
      <pubDate>Mon, 19 Oct 2026 23:35:54 +0000</pubDate>
      <link>https://dev.to/nebosa_company_0edc92e18b/building-a-no-code-app-builder-out-of-small-pure-modules-and-testing-each-one-on-its-own-5aa3</link>
      <guid>https://dev.to/nebosa_company_0edc92e18b/building-a-no-code-app-builder-out-of-small-pure-modules-and-testing-each-one-on-its-own-5aa3</guid>
      <description>&lt;p&gt;I'm a solo founder based in Europe, building an app builder (Appdor) that turns a spreadsheet into a small business app. This post is about how the code is organised, because that choice has mattered more than any framework decision. Appdor is a plain JavaScript single-page app on Vite, with Postgres behind it. There is no UI framework doing the heavy lifting. What does the heavy lifting is a rule I set early.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: every feature starts as a pure module
&lt;/h2&gt;

&lt;p&gt;A feature like "sort and filter rows" or "evaluate a formula" begins as a function (or a small set of functions) that takes data and returns data. No DOM, no network, no globals. It lives in its own folder under &lt;code&gt;src/&lt;/code&gt; and has a mirrored test file. The thin layer that touches the page is written afterwards, and is deliberately dull.&lt;/p&gt;

&lt;p&gt;Two examples from the repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The formula engine&lt;/strong&gt; (&lt;code&gt;src/formula/&lt;/code&gt;): a tokenizer, a parser, an evaluator and a function registry, each in its own file, plus a dialect layer that translates formulas written for other spreadsheets. Because the evaluator only sees values and a registry, a test can feed it a string and assert on the result. There is also a compiler that turns a subset of formulas into SQL, so the same expression can run on the server when the data is too big to bring to the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The views engine&lt;/strong&gt; (&lt;code&gt;src/views/&lt;/code&gt;): sort, filter, group and summarise over an array of records. The grid, the board and the calendar all call the same function and differ only in how they draw the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this pays off
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Tests are cheap, so they exist.&lt;/em&gt; A pure function needs no browser or mock server. The suite has thousands of tests and runs quickly because most of the code under test never touches a page.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;The same module runs in more than one place.&lt;/em&gt; Validation, for instance, runs when you edit a cell, when you submit a public form and when you import a file. One implementation means one set of bugs.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;The glue layer stays thin enough to read.&lt;/em&gt; The grid's cell editing is a good example. A small module decides how to coerce and validate what you typed; the DOM code just calls it and shows the toast.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where it hurts (honest list)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One large file never got split.&lt;/strong&gt; A single module, the main application file, grew into a tangle where about a third of the functions depend on each other in a cycle. Pure modules around it are tidy; the file itself is not, and extracting a function out of it is harder than it should be because one test suite loads parts of it as text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard rails become part of the product.&lt;/strong&gt; Several rules are enforced by tests that read the source: a bundle-size budget that fails the build, a check that every user-visible string has a translation key, and a check that new modules are actually imported from somewhere. They catch real mistakes, but they also fail for boring reasons, and they depend on the tree being stable when they run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Tested" is not "reachable".&lt;/strong&gt; I once shipped a module with good tests that nothing imported. The suite was green, and the feature did not exist in the built bundle. Now there is a check that greps the output, not just the import graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure modules don't remove integration bugs.&lt;/strong&gt; A date handed from one module to another in the wrong shape passes both modules' tests. You still need a few tests that go through the real glue, and a real browser run before you trust a release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd do again
&lt;/h2&gt;

&lt;p&gt;Start with the data structure and the function that transforms it. Write the failing test with a hand-made input. Only then draw anything. When I broke this order, I ended up with logic buried in event handlers that I could only test by clicking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Set a rule on day one that no file may import from a "god module", and ban "temporary" functions that hold state at module level. Both are cheaper to prevent than to remove.&lt;/p&gt;

&lt;p&gt;If you build UI-heavy tools in plain JS, how do you keep logic out of the event handlers? I'd like to compare notes. (Appdor is at appdor.com if you're curious what these modules add up to; it's an early product from one person, with plenty of rough edges.)&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>architecture</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Airtable formulas are the migration. Everything else is a CSV.</title>
      <dc:creator>Nebosa Company</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/nebosa_company_0edc92e18b/your-airtable-formulas-are-the-migration-everything-else-is-a-csv-2je9</link>
      <guid>https://dev.to/nebosa_company_0edc92e18b/your-airtable-formulas-are-the-migration-everything-else-is-a-csv-2je9</guid>
      <description>&lt;p&gt;Getting your records out of Airtable is not the hard part. There is a button&lt;br&gt;
for it and the rows come out fine.&lt;/p&gt;

&lt;p&gt;The hard part is that a CSV export gives you what each formula column &lt;em&gt;last&lt;br&gt;
said&lt;/em&gt;, not what it means. &lt;code&gt;Days to run&lt;/code&gt; arrives as &lt;code&gt;91&lt;/code&gt; — a number, frozen, in&lt;br&gt;
a column that used to update itself. Multiply that by however many formula&lt;br&gt;
columns two years of use accumulated, across however many bases, and you have&lt;br&gt;
the real migration: not moving data, but rebuilding logic from memory while&lt;br&gt;
looking at the answers it used to produce.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" alt="Appdor in 26 seconds: grid, approval workflow, kanban, dashboard"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The product tour, silent by design. &lt;a href="https://appdor.com/media/homepage/tour.webm" rel="noopener noreferrer"&gt;Full-resolution video&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The failure is quiet, which is why it stalls migrations
&lt;/h2&gt;

&lt;p&gt;It does not announce itself. The data lands, people start using the new tool,&lt;br&gt;
and three weeks later somebody notices a column has been frozen since the&lt;br&gt;
switch. Nobody knows which columns those are, because the export did not&lt;br&gt;
distinguish a formula from a number. So somebody opens Airtable in a second&lt;br&gt;
window and starts retyping expressions.&lt;/p&gt;

&lt;p&gt;That work is unbounded and boring, which means it gets postponed, which means&lt;br&gt;
the two systems run in parallel, which is how a migration reverts.&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually happens to an Airtable formula
&lt;/h2&gt;

&lt;p&gt;Appdor reads 7 formula dialects and compiles them into its own&lt;br&gt;
expression language, so the &lt;em&gt;expression&lt;/em&gt; moves and stays live. Here is what&lt;br&gt;
that looks like on real formulas — left column in, right column out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Airtable&lt;/th&gt;
&lt;th&gt;Appdor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IF({Status} = "Done", 1, IF({Status} = "Blocked", -1, 0))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IF({Status} = "Done", 1, IF({Status} = "Blocked", -1, 0))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATETIME_DIFF({Due}, {Start}, 'days')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DATETIME_DIFF({Due}, {Start}, "days")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATETIME_FORMAT({Due}, 'YYYY-MM-DD')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DATETIME_FORMAT({Due}, "YYYY-MM-DD")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROUND({Hours} * 145, 2)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROUND({Hours} * 145, 2)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SWITCH({Tier}, "Priority", 2, "Standard", 1, 0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SWITCH({Tier}, "Priority", 2, "Standard", 1, 0)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REGEX_EXTRACT({Project}, '^[A-Z]{3}')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;REGEXEXTRACT({Project}, "^[A-Z]{3}")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ARRAYJOIN({Tags}, ", ")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;JOIN({Tags}, ", ")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RECORD_ID()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROWID()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CREATED_TIME()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATEDON()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things are worth noticing, and neither is the flashy one.&lt;/p&gt;

&lt;p&gt;The first five rows are boring: the nested conditional, the date arithmetic,&lt;br&gt;
the rounding and the &lt;code&gt;SWITCH&lt;/code&gt; come across effectively unchanged, with quoting&lt;br&gt;
normalised. That is the common case, and the common case being boring is the&lt;br&gt;
entire product claim.&lt;/p&gt;

&lt;p&gt;The last four are the interesting ones — &lt;code&gt;REGEX_EXTRACT&lt;/code&gt; loses its underscore,&lt;br&gt;
&lt;code&gt;ARRAYJOIN&lt;/code&gt; becomes &lt;code&gt;JOIN&lt;/code&gt;, &lt;code&gt;RECORD_ID&lt;/code&gt; becomes &lt;code&gt;ROWID&lt;/code&gt;, &lt;code&gt;CREATED_TIME&lt;/code&gt; becomes&lt;br&gt;
&lt;code&gt;CREATEDON&lt;/code&gt;. These are the ones a person retyping by hand gets wrong, because&lt;br&gt;
they &lt;em&gt;look&lt;/em&gt; fine either way and only fail at evaluation.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it refuses to do
&lt;/h2&gt;

&lt;p&gt;Some Airtable functions have no counterpart, and the compiler says so rather&lt;br&gt;
than guessing. &lt;code&gt;PREVIOUS_VALUE({Status})&lt;/code&gt; depends on Airtable keeping the prior&lt;br&gt;
value of a cell — a model Appdor does not have — and it comes back like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unknown function "PREVIOUS_VALUE" in dialect airtable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A formula that does not parse at all gets a position, not a shrug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Could not parse airtable formula: Unexpected character '@' at position 28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That list of refusals is your actual migration backlog, and it is normally far&lt;br&gt;
shorter than the inventory of formula columns. It is also the part worth&lt;br&gt;
insisting on when you evaluate anything else in this category: &lt;strong&gt;a migration&lt;br&gt;
tool that reports no losses has not found none, it has stopped looking.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the base moves too, and it also reports
&lt;/h2&gt;

&lt;p&gt;Formulas are the headline, but they are not the only thing a CSV drops.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linked records.&lt;/strong&gt; A link cell holds Airtable's own &lt;code&gt;rec…&lt;/code&gt; ids, and those
mean nothing once rows have new ids. So the import runs in two passes: write
every row, build a map of old id to new id, then go back and rewrite the
links. Doing it in one pass is the obvious implementation and it silently
produces a base where every cross-record link points at nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saved views.&lt;/strong&gt; Filters and sorts are rebuilt against the new columns. A
clause that cannot be expressed is not dropped quietly — in a real run, a view filtered on a checkbox lost its clause and arrived showing every
row, and the report raised it as &lt;em&gt;blocked&lt;/em&gt; rather than as a note. A view that
arrives unfiltered shows &lt;strong&gt;more&lt;/strong&gt; rows than it should, and for a view somebody
shares, that is a disclosure rather than a cosmetic loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buttons, interfaces and automations.&lt;/strong&gt; These have no equivalent. The button
arrives as text, and the report says the type had no equivalent rather than
leaving a column that looks right and does nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both import routes show you what they are about to do before anything is&lt;br&gt;
written, and both name what they could not carry — the formulas they&lt;br&gt;
translated, the field types that fell back to text, and the ones that failed&lt;br&gt;
outright. They are not identical: the pasted-export route ends in a full&lt;br&gt;
&lt;strong&gt;fidelity report&lt;/strong&gt; that sorts every column into carried exactly, approximated,&lt;br&gt;
unsupported or dropped, and adds findings about saved views and relationships&lt;br&gt;
that the token route's preview does not raise. Both tell you where the losses&lt;br&gt;
are; one of them is more thorough about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order that works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventory the formula columns before you migrate&lt;/strong&gt;, while you still have
the old system to read. This is twenty minutes and it is the step people
skip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import and read the report.&lt;/strong&gt; Nothing is written until you have. The
refusals list is the backlog; the blocked items are the ones that change
what people see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild the refusals by hand.&lt;/strong&gt; There will be some. There are always some.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff a sample of computed values against Airtable before you switch anyone
over.&lt;/strong&gt; This is the only step that proves the migration worked, and it is
the other one people skip.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1frwo50ijole68fcfb3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1frwo50ijole68fcfb3y.png" alt="An approval workflow in Appdor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the import gets your base
&lt;/h2&gt;

&lt;p&gt;For completeness, since this is the question every thread about this asks: you&lt;br&gt;
make a read-only personal access token in Airtable — &lt;code&gt;schema.bases:read&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;data.records:read&lt;/code&gt; — paste it into Settings → Import, and Appdor lists your&lt;br&gt;
bases so you can pick one. The token goes straight to the server and never&lt;br&gt;
stays in your browser; it is discarded after the request unless you ask us to&lt;br&gt;
remember it, in which case it is encrypted at rest and only its last four&lt;br&gt;
characters are ever shown back to you. There is no OAuth flow and nobody should&lt;br&gt;
tell you there is one click.&lt;/p&gt;

&lt;p&gt;There is also a paste route, for a base you no longer have access to or an&lt;br&gt;
export somebody sent you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://appdor.com/blog/10-migrating-formulas-off-airtable" rel="noopener noreferrer"&gt;appdor.com&lt;/a&gt;. I build Appdor, so weigh accordingly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>airtable</category>
      <category>database</category>
      <category>nocode</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your data should outlive my company</title>
      <dc:creator>Nebosa Company</dc:creator>
      <pubDate>Tue, 06 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/nebosa_company_0edc92e18b/your-data-should-outlive-my-company-3dob</link>
      <guid>https://dev.to/nebosa_company_0edc92e18b/your-data-should-outlive-my-company-3dob</guid>
      <description>&lt;p&gt;On 4 August 2026 Airtable was bought by Bending Spoons for $1.285bn.&lt;/p&gt;

&lt;p&gt;I am not going to tell you what happens to your bill. I don't know, and neither&lt;br&gt;
does anyone else writing about it. What I will say is that the&lt;br&gt;
acquirer has a public record: Evernote went from 341 people to 60 inside twelve&lt;br&gt;
months and the free plan was curtailed; WeTransfer saw staff cut and free limits&lt;br&gt;
tightened; TechCrunch traced the same cycle across five of their purchases.&lt;/p&gt;

&lt;p&gt;I build a competing product, so discount me accordingly. But the question stands&lt;br&gt;
whether you ever talk to me or not, and it isn't really about Airtable. It's&lt;br&gt;
this: if the company holding your operational data changed its mind about you&lt;br&gt;
tomorrow, what would you actually do?&lt;/p&gt;

&lt;p&gt;For most teams the honest answer is "export a CSV and hope." That is the problem&lt;br&gt;
I have spent this year on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" alt="Appdor in 26 seconds: grid, approval workflow, kanban, dashboard" width="720" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The product tour, silent by design. &lt;a href="https://appdor.com/media/homepage/tour.webm" rel="noopener noreferrer"&gt;Full-resolution video&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What "keep your own data" has to mean
&lt;/h2&gt;

&lt;p&gt;It is a phrase every vendor uses and almost none of them mean, because meaning&lt;br&gt;
it is expensive. Here is the version I hold myself to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has to run without me.&lt;/strong&gt; Not "we offer an enterprise self-hosted tier." Not&lt;br&gt;
"contact sales." The whole install is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.appdor.com/appdor-latest.tar.gz | docker load
curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://get.appdor.com/docker-compose.yml
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; ghcr.io/nebosa-company/appdor generate-keys &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One container: the database, the API and the app together. Thirty days on the&lt;br&gt;
Business tier, then a licence — a signed file checked against a key inside the&lt;br&gt;
image, so nothing phones home to ask my permission to keep working. If I am hit&lt;br&gt;
by a bus, the licence you hold keeps working until the date printed in it, and&lt;br&gt;
your data stays readable and exportable after that with no licence at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has to be truthful about telemetry.&lt;/strong&gt; There &lt;em&gt;is&lt;/em&gt; a crash reporter and there&lt;br&gt;
&lt;em&gt;are&lt;/em&gt; product analytics in the box. I would rather describe that accurately than&lt;br&gt;
write "no telemetry" and have you find the consent banner yourself. The crash&lt;br&gt;
pipeline stays inert until you set a key. The analytics ask first, honour Do Not&lt;br&gt;
Track, and post to your own instance rather than to me. The prompt the product&lt;br&gt;
actually shows on first run says it records page names and actions — never your&lt;br&gt;
data, never your record contents — and if you press &lt;em&gt;No thanks&lt;/em&gt;, nothing is&lt;br&gt;
recorded and it does not ask again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has to be checkable rather than promised.&lt;/strong&gt; Two scripts ship in the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/rls-test.sh
bash scripts/tenant-isolation-test.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second one proves two tenants cannot see each other in &lt;em&gt;both&lt;/em&gt; modes — shared&lt;br&gt;
database and database-per-tenant. You run it against your own install. You don't&lt;br&gt;
take my word for the isolation, and you shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product this is attached to
&lt;/h2&gt;

&lt;p&gt;None of the above matters if what you are self-hosting is a toy. The application&lt;br&gt;
I use to test this is a plant-wide manufacturing ERP: employees and gate entry,&lt;br&gt;
shifts and vacation, working stands, SOPs, items, bill of materials, warehouses&lt;br&gt;
and inventory, suppliers, purchase orders, customers, sales and production&lt;br&gt;
orders, quality, repairs, forwarders and shipments. Twenty-two pages, eleven&lt;br&gt;
workflows, one workspace.&lt;/p&gt;

&lt;p&gt;Four things in there worth a technical reader's attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Field types that refuse bad input
&lt;/h3&gt;

&lt;p&gt;Your database lets you type an IBAN wrong. So does every spreadsheet-shaped tool&lt;br&gt;
I have compared against — they store it as single-line text and hope, and you&lt;br&gt;
find out the money is gone about a week later. There are 90 field&lt;br&gt;
types here and 59 of them refuse malformed input by format&lt;br&gt;
or checksum.&lt;/p&gt;

&lt;p&gt;Eleven of those are enforced in the database itself rather than in the browser:&lt;br&gt;
IBAN and LEI by ISO 7064 mod-97, ISIN and card numbers and IMEI by Luhn, the VIN&lt;br&gt;
transliteration, ISBN-10, ISBN-13 and the bare ISBN, EAN-13, UPC. Those hold&lt;br&gt;
when the row arrives through the REST API, a webhook, or an importer at 3am with&lt;br&gt;
no browser in the loop.&lt;/p&gt;

&lt;p&gt;The rest — VAT, MIC, ICD-10, SNOMED, NDC, HS codes, UN/LOCODE — validate as you&lt;br&gt;
type, and that is deliberate rather than unfinished. Their rules are national&lt;br&gt;
registries and revisable code lists. A regex that is subtly wrong at the&lt;br&gt;
database level rejects a real customer's valid identifier with no way around it.&lt;br&gt;
A mod-97 cannot be subtly wrong. Narrow and correct beats broad and approximate&lt;br&gt;
on the write path.&lt;/p&gt;

&lt;h3&gt;
  
  
  A dashboard is just a page
&lt;/h3&gt;

&lt;p&gt;There is no separate BI product bolted on the side. A page is a stack of&lt;br&gt;
elements, and there are 48 of them — table, list, gallery, kanban,&lt;br&gt;
calendar, chart, form, gantt, timeline, pivot and the rest. You drop a number&lt;br&gt;
tile on a page, point it at a table and an aggregate, and that is your&lt;br&gt;
dashboard. The morning-meeting screen for the ERP above is three number tiles&lt;br&gt;
and a chart: order book, output and stock, on one page.&lt;/p&gt;

&lt;p&gt;Every chart type and every view is on the Free tier. That is an easier promise&lt;br&gt;
to keep when the charts are not a separate product with their own margin to&lt;br&gt;
defend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automations you can audit
&lt;/h3&gt;

&lt;p&gt;Rules are the part of a low-code tool that rots quietly. Somebody builds one,&lt;br&gt;
leaves, and two years later nobody can say what fires, or whether it still does.&lt;/p&gt;

&lt;p&gt;So the workflow list is workspace-wide rather than buried one table at a time.&lt;br&gt;
Each rule shows its trigger, its condition, the app and table it belongs to,&lt;br&gt;
whether it is on — and the part I care about most, whether it has ever run.&lt;br&gt;
"Never used" is a badge on the row, not something you go digging through logs to&lt;br&gt;
work out. The rules marked that way are the ones worth deleting, and the page&lt;br&gt;
turns that into a five-second judgement instead of an afternoon.&lt;/p&gt;

&lt;h3&gt;
  
  
  A seam for agents instead of a fight with them
&lt;/h3&gt;

&lt;p&gt;"AI will make no-code obsolete" is half right: generating an interface is nearly&lt;br&gt;
free now, and the moat no-code had in 2019 is gone. The half that is not right&lt;br&gt;
is that your data still has to live somewhere — with permissions, with a backup&lt;br&gt;
you have actually tested, with something stopping the malformed row, with an&lt;br&gt;
audit trail when someone asks who changed the price. AI does not remove that. It&lt;br&gt;
makes it easier to skip until it hurts.&lt;/p&gt;

&lt;p&gt;So there is an MCP server in the box, over stdio and HTTP. Your database becomes&lt;br&gt;
a tool the agent holds, scoped by row-level security, rather than something&lt;br&gt;
reached through a scraped API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI meter I deleted
&lt;/h2&gt;

&lt;p&gt;On the 4th of August I deleted our AI billing meter. Not disabled — deleted.&lt;br&gt;
There is no credit balance in the product, nothing to debit, and no code path&lt;br&gt;
that can refuse your AI call to protect my margin.&lt;/p&gt;

&lt;p&gt;To be precise, because someone will eventually read the code and I would rather&lt;br&gt;
they find me accurate than clever: usage is still &lt;strong&gt;recorded&lt;/strong&gt;. Which model ran,&lt;br&gt;
for which feature, on whose behalf, and how many tokens your provider counted.&lt;br&gt;
That row exists so an admin can check it against their own provider invoice.&lt;br&gt;
Recording is not metering — nothing in that row decides whether your next&lt;br&gt;
operation is allowed, and no number in it is a price.&lt;/p&gt;

&lt;p&gt;The reason is that I could not find an honest version of the alternative. Every&lt;br&gt;
AI credit pack I priced came out as a markup on someone else's model, dressed up&lt;br&gt;
as a feature. You would be paying me to pass your prompt along. So instead:&lt;br&gt;
bring your own key. Your provider, your rate, your bill. I never see it and I&lt;br&gt;
take no cut, on every plan including Free.&lt;/p&gt;

&lt;p&gt;The one thing that can stop an AI call is a spend cap you set and you raise, so&lt;br&gt;
a runaway workflow cannot burn through your provider key at 3am. I do not earn a&lt;br&gt;
penny from it firing.&lt;/p&gt;

&lt;p&gt;This removes a revenue line most of my category treats as core. I would rather&lt;br&gt;
explain that to an investor than explain a credit pack to a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I don't have
&lt;/h2&gt;

&lt;p&gt;I am one engineer, and the fastest way to lose a technical reader is to let them&lt;br&gt;
discover a gap I hid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We are not SOC 2 certified. Not ISO 27001. No HIPAA BAA.&lt;/strong&gt; The test suite&lt;br&gt;
fails the build if those words appear on the homepage, which was deliberate. If&lt;br&gt;
a certificate is a hard gate for you this quarter, I am the wrong call this&lt;br&gt;
quarter, and I will tell you that on the first call rather than the fifth.&lt;/p&gt;

&lt;p&gt;What exists instead is measurement, and you can read the recordings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Disaster recovery&lt;/td&gt;
&lt;td&gt;RTO 9.4s, RPO 120 rows, restore verified by checksum&lt;/td&gt;
&lt;td&gt;1 August, due a re-run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy-neighbour fairness&lt;/td&gt;
&lt;td&gt;One tenant under a 3,000-request storm from another ran at 0.89x its own quiet p99&lt;/td&gt;
&lt;td&gt;September 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load&lt;/td&gt;
&lt;td&gt;p50 37ms, p99 43ms, zero errors over 500 requests&lt;/td&gt;
&lt;td&gt;September 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An older load run on a smaller database read p50 6ms. I am quoting the slower&lt;br&gt;
number because it is the current one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The repository is private while this is early.&lt;/strong&gt; So "run the isolation test&lt;br&gt;
yourself" today means asking me for access rather than a public clone. That is a&lt;br&gt;
decision I intend to reverse, not a permanent one, and I would rather say it&lt;br&gt;
plainly than link you into a 404.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is one person.&lt;/strong&gt; That is the risk you take on, and it is exactly why the&lt;br&gt;
self-hosted path above is not a marketing tier. It is the answer to the obvious&lt;br&gt;
objection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbioc5nxv438l72aer4m8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbioc5nxv438l72aer4m8.png" alt="A grid view in Appdor, running on your own Postgres" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to look
&lt;/h2&gt;

&lt;p&gt;There are no seat minimums, no charge for read-only viewers, no charge for the&lt;br&gt;
people who use the apps you publish, and no view or chart type held behind a&lt;br&gt;
tier.&lt;/p&gt;

&lt;p&gt;If you are weighing a move this quarter, the useful ten minutes is not reading&lt;br&gt;
this post — it is opening your current bill and counting how many editors you&lt;br&gt;
pay for versus how many actually edit. Almost nobody knows that number offhand.&lt;/p&gt;

&lt;p&gt;And if you want to test the switching cost rather than guess at it: send me your&lt;br&gt;
gnarliest formula from Airtable, monday, ClickUp, Smartsheet, Quickbase, Notion&lt;br&gt;
or AppSheet. 7 dialects are understood inbound, 279 functions on this&lt;br&gt;
side to land them in. I will paste it in and screen-record what happens —&lt;br&gt;
working or not. I will show you the failures too. Those are more useful to me&lt;br&gt;
than the wins.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://appdor.com/blog/09-your-data-should-outlive-my-company" rel="noopener noreferrer"&gt;appdor.com&lt;/a&gt;. I build Appdor, so weigh accordingly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>postgres</category>
      <category>opensource</category>
      <category>database</category>
    </item>
    <item>
      <title>The seat minimum is a pricing decision, and you are paying for it</title>
      <dc:creator>Nebosa Company</dc:creator>
      <pubDate>Tue, 29 Sep 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/nebosa_company_0edc92e18b/the-seat-minimum-is-a-pricing-decision-and-you-are-paying-for-it-34f3</link>
      <guid>https://dev.to/nebosa_company_0edc92e18b/the-seat-minimum-is-a-pricing-decision-and-you-are-paying-for-it-34f3</guid>
      <description>&lt;p&gt;Here is a thing that is easy to miss when you compare pricing pages: the number&lt;br&gt;
on the page is the &lt;em&gt;rate&lt;/em&gt;, not the &lt;em&gt;bill&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Two of the biggest tools in this category will not sell you exactly the number&lt;br&gt;
of seats you have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" alt="Appdor in 26 seconds: grid, approval workflow, kanban, dashboard" width="720" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The product tour, silent by design. &lt;a href="https://appdor.com/media/homepage/tour.webm" rel="noopener noreferrer"&gt;Full-resolution video&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  monday.com sells in blocks of five, with a floor of three
&lt;/h2&gt;

&lt;p&gt;The Pro plan is $19 per editor per month. But the smallest purchase is&lt;br&gt;
3 seats, and above that it is sold in blocks of 5.&lt;/p&gt;

&lt;p&gt;So if your team is six people, you do not buy six seats. You buy ten.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Editors you have&lt;/th&gt;
&lt;th&gt;Seats monday bills&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;th&gt;Effective rate per real editor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;$95&lt;/td&gt;
&lt;td&gt;$95.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;$190&lt;/td&gt;
&lt;td&gt;$31.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;$285&lt;/td&gt;
&lt;td&gt;$25.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;$380&lt;/td&gt;
&lt;td&gt;$23.75&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of those rows is a real invoice, and every figure in the table came&lt;br&gt;
out of the function our own pricing page bills against rather than out of my&lt;br&gt;
head. At six editors you pay for ten at $19 each, so the rate per person&lt;br&gt;
you actually have is &lt;strong&gt;$19 on the pricing page and $31.67 in practice&lt;/strong&gt; —&lt;br&gt;
67% higher.&lt;/p&gt;

&lt;p&gt;Note the first row. The floor is 3 seats but the block size is&lt;br&gt;
5, so a single editor is rounded up to 5 rather than&lt;br&gt;
to the floor. The two rules compound; they do not overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quickbase will not sell you fewer than twenty
&lt;/h2&gt;

&lt;p&gt;The Team plan is $35 per user per month with a floor of&lt;br&gt;
20 users. That is the entry price of the product, and it is the&lt;br&gt;
entry price whether you are twenty people or three.&lt;/p&gt;

&lt;p&gt;For a team of five evaluating it, the pricing page says $35 and the&lt;br&gt;
invoice says &lt;strong&gt;$700 a month&lt;/strong&gt; — 20 seats at $35,&lt;br&gt;
four times the people you have. That is not a trick; it is on their page. But it&lt;br&gt;
is the number that decides whether you can pilot the thing, and it is not the&lt;br&gt;
number anyone quotes in a meeting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why vendors do this
&lt;/h2&gt;

&lt;p&gt;Not out of malice. Seat floors and buckets do three things for a vendor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They raise average contract value without raising the advertised rate, which
is the number that gets compared.&lt;/li&gt;
&lt;li&gt;They make small accounts profitable to support.&lt;/li&gt;
&lt;li&gt;They create a step function, so growth produces revenue in jumps rather than
smoothly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third is the one to watch, because it means your bill moves when you hire&lt;br&gt;
your eleventh editor and does not move when you hire your twelfth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Appdor does
&lt;/h2&gt;

&lt;p&gt;No minimum, and no buckets. You are billed for the editors you have, viewers are&lt;br&gt;
free and unlimited, and the plan does not decide which of the product you may&lt;br&gt;
use — all 48 page elements, 25 chart types and&lt;br&gt;
24 dashboard widgets are in every tier.&lt;/p&gt;

&lt;p&gt;⚠️ That is a pricing commitment, not a claim of being cheapest. For a large team&lt;br&gt;
on an annual deal, a competitor's negotiated rate may well beat our list price.&lt;br&gt;
What we are saying is narrower and checkable: &lt;strong&gt;you will not be invoiced for&lt;br&gt;
seats you do not have.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want the comparison for your own headcount rather than a table of&lt;br&gt;
somebody else's, the migration calculator on the pricing page does the&lt;br&gt;
arithmetic — including the floors and the buckets, because a saving computed&lt;br&gt;
against the advertised rate rather than the real invoice would be the same&lt;br&gt;
sleight of hand this post is about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3vjgui9enbfnf71d6hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3vjgui9enbfnf71d6hx.png" alt="A dashboard in Appdor: counts, activity chart, status donut" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The wider point
&lt;/h2&gt;

&lt;p&gt;Seat rules are the clearest example of something true across this whole&lt;br&gt;
category: &lt;strong&gt;the pricing page is a marketing surface and the invoice is the&lt;br&gt;
product.&lt;/strong&gt; When you evaluate any of these tools, price the invoice for the team&lt;br&gt;
you actually have, at the size you expect to be in a year, with the features you&lt;br&gt;
will actually need — and check which of those features is a tier.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://appdor.com/blog/05-seat-minimums" rel="noopener noreferrer"&gt;appdor.com&lt;/a&gt;. I build Appdor, so weigh accordingly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>pricing</category>
      <category>startup</category>
      <category>nocode</category>
    </item>
    <item>
      <title>The container was running software nobody built</title>
      <dc:creator>Nebosa Company</dc:creator>
      <pubDate>Tue, 22 Sep 2026 17:08:24 +0000</pubDate>
      <link>https://dev.to/nebosa_company_0edc92e18b/the-container-was-running-software-nobody-built-3lmm</link>
      <guid>https://dev.to/nebosa_company_0edc92e18b/the-container-was-running-software-nobody-built-3lmm</guid>
      <description>&lt;p&gt;A deploy that silently does nothing is worse than a deploy that fails, and we shipped two flavours of it before either one announced itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first one: a deploy with an expiry date
&lt;/h2&gt;

&lt;p&gt;The fast dev loop is to tar a built &lt;code&gt;dist/&lt;/code&gt; into &lt;code&gt;/usr/share/nginx/html&lt;/code&gt; of a running container. It works. The page updates, the change is live, and the loop is seconds instead of minutes.&lt;/p&gt;

&lt;p&gt;It survives &lt;code&gt;docker restart&lt;/code&gt;. It does not survive a &lt;code&gt;docker compose up -d&lt;/code&gt; that recreates the container, because the new container comes up from the image, and everything deployed that way reverts to whatever the image was built with.&lt;/p&gt;

&lt;p&gt;Nothing errors when this happens. No log line, no failed request; the app simply becomes an older app, and every measurement taken afterwards is of software nobody built.&lt;/p&gt;

&lt;p&gt;It cost one session a reskin, a glyph set and two bug fixes on 2026-08-27. It was noticed only because the served bundle hash changed — not because anything complained.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second one: a recreate is not a rebuild
&lt;/h2&gt;

&lt;p&gt;The subtler version has no hot-patching in it at all.&lt;/p&gt;

&lt;p&gt;Measured the same day: the running image had been built the previous evening and carried &lt;strong&gt;19 supervisord programs against the tree's 20&lt;/strong&gt;. The missing one was &lt;code&gt;cert-watch&lt;/code&gt;, the certificate-expiry watcher. So on a deployment serving portal custom domains, the alarm for a certificate about to expire was not running — and the image's own healthcheck was one probe short of being able to notice that it wasn't.&lt;/p&gt;

&lt;p&gt;The instinct here is &lt;code&gt;--force-recreate&lt;/code&gt;, and it does not help. It recreates the container &lt;em&gt;from the same image&lt;/em&gt;. Only &lt;code&gt;docker compose build&lt;/code&gt; produces a new one. The distance between those two commands is one word, and the feedback that tells them apart is nothing at all: &lt;code&gt;docker ps&lt;/code&gt;, &lt;code&gt;docker inspect&lt;/code&gt; and the health status all look identical either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "it looks fine" is not evidence
&lt;/h2&gt;

&lt;p&gt;Both failures share a shape worth naming, because it generalises past Docker.&lt;/p&gt;

&lt;p&gt;A deployment has two states that present identically to every casual check: &lt;em&gt;running what you built&lt;/em&gt;, and &lt;em&gt;running something older that still starts cleanly&lt;/em&gt;. Nothing in the second state is broken enough to complain. The process is up. The healthcheck passes — it is the old healthcheck, and it passes honestly. Requests return 200. The only difference is that the thing answering them is not the thing in your tree.&lt;/p&gt;

&lt;p&gt;The measurement is what gives the game away, in both directions. A bundle budget that improved without anybody optimising anything. A test that started passing without anybody fixing it. Both of those are the same report: you are measuring a build you did not make.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we do about it
&lt;/h2&gt;

&lt;p&gt;Three things, none of them clever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name the fast path as a dev loop, in the script itself.&lt;/strong&gt; &lt;code&gt;scripts/deploy-to-container.sh&lt;/code&gt; opens with the line &lt;code&gt;NOT A DEPLOY. See docs/deployment.md — a release is an image. This is the dev loop.&lt;/code&gt; A comment is not a gate, but the cost of this failure is entirely in the belief that a deploy happened, so the belief is what to attack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have a command that answers "what is actually running".&lt;/strong&gt; &lt;code&gt;node scripts/env-preflight.mjs&lt;/code&gt; diffs the container's &lt;code&gt;[program:]&lt;/code&gt; set against &lt;code&gt;docker/supervisord.conf&lt;/code&gt; and names the programs that are missing. That is the check that catches the 19-against-20 case, and it exists because nothing in Docker's own output does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give the database question its own tool, with three outcomes.&lt;/strong&gt; &lt;code&gt;npm run ops:drift&lt;/code&gt; compares the init files against the database you are serving from, and it reports &lt;code&gt;IN SYNC&lt;/code&gt; (0), &lt;code&gt;DRIFT&lt;/code&gt; (1) or &lt;code&gt;NOT MEASURED&lt;/code&gt; (2) when the database could not be read. The third one matters more than it looks: an unmeasured state must never be readable as a good one. Most tooling folds "I couldn't check" into "fine", which is how you end up trusting a green light that was never on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule this leaves us with
&lt;/h2&gt;

&lt;p&gt;If you recreate the container, redeploy. And if a number moves in a direction nobody worked for, check which build is serving before you believe it.&lt;/p&gt;

&lt;p&gt;That is not a Docker lesson. It is what happens any time the artefact you deploy and the artefact you edit are two different objects, and only one of them is on screen.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Appdor vs Airtable: the same job, a different bill and a different address</title>
      <dc:creator>Nebosa Company</dc:creator>
      <pubDate>Tue, 22 Sep 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/nebosa_company_0edc92e18b/appdor-vs-airtable-the-same-job-a-different-bill-and-a-different-address-5g1e</link>
      <guid>https://dev.to/nebosa_company_0edc92e18b/appdor-vs-airtable-the-same-job-a-different-bill-and-a-different-address-5g1e</guid>
      <description>&lt;p&gt;Airtable made this category. If you are reading this you have probably built&lt;br&gt;
something real in it, it probably works, and the question is not whether&lt;br&gt;
Airtable is good software. It is whether the two things it cannot change about&lt;br&gt;
itself matter to you.&lt;/p&gt;

&lt;p&gt;Those two things are &lt;strong&gt;where your rows live&lt;/strong&gt; and &lt;strong&gt;what the second year costs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" alt="Appdor in 26 seconds: grid, approval workflow, kanban, dashboard" width="720" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The product tour, silent by design. &lt;a href="https://appdor.com/media/homepage/tour.webm" rel="noopener noreferrer"&gt;Full-resolution video&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the rows live
&lt;/h2&gt;

&lt;p&gt;Airtable is a hosted product. Your data is in their infrastructure, and the&lt;br&gt;
answer to "which sub-processors touch our production records" is their list, not&lt;br&gt;
yours.&lt;/p&gt;

&lt;p&gt;Appdor is a work OS that sits &lt;strong&gt;on top of&lt;/strong&gt; a Postgres database rather than&lt;br&gt;
inside a vendor's. The database is the system of record, it is ordinary&lt;br&gt;
Postgres, and you can point &lt;code&gt;psql&lt;/code&gt; at it. The whole install is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.appdor.com/appdor-latest.tar.gz | docker load
curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://get.appdor.com/docker-compose.yml
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; ghcr.io/nebosa-company/appdor generate-keys &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a lot of teams that difference is worth nothing, and I would rather say so&lt;br&gt;
than pretend otherwise. It becomes worth something the first time a customer&lt;br&gt;
contract names a jurisdiction, an auditor asks for the schema, or somebody wants&lt;br&gt;
a read-replica for the warehouse team.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the second year costs
&lt;/h2&gt;

&lt;p&gt;This is the part that surprises people, because it is not the sticker price.&lt;/p&gt;

&lt;p&gt;Airtable's Team plan is $20 per editor per month on annual billing;&lt;br&gt;
Business is $45. The jump between them is the interesting number:&lt;br&gt;
it is a &lt;strong&gt;125% increase&lt;/strong&gt;, and the features that force it — field-level&lt;br&gt;
permissions, SAML, admin controls, more records per base — are the ones a team&lt;br&gt;
needs precisely when it has stopped being a pilot.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Airtable&lt;/th&gt;
&lt;th&gt;Appdor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entry paid tier&lt;/td&gt;
&lt;td&gt;$20 per editor / month&lt;/td&gt;
&lt;td&gt;see the pricing page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next tier up&lt;/td&gt;
&lt;td&gt;$45 per editor / month&lt;/td&gt;
&lt;td&gt;one step, not a cliff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Views behind a plan&lt;/td&gt;
&lt;td&gt;Gantt and timeline are paid tiers&lt;/td&gt;
&lt;td&gt;all 12, every plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts behind a plan&lt;/td&gt;
&lt;td&gt;extensions vary by tier&lt;/td&gt;
&lt;td&gt;all 25, every plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Records per base&lt;/td&gt;
&lt;td&gt;capped, and the cap is a tier feature&lt;/td&gt;
&lt;td&gt;your Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read-only viewers&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosting&lt;/td&gt;
&lt;td&gt;not offered&lt;/td&gt;
&lt;td&gt;the same build you would pay us to host&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The row that matters most is the one about views. Appdor ships 12 view&lt;br&gt;
types, 48 page elements, 25 chart types and&lt;br&gt;
24 dashboard widgets, and &lt;strong&gt;none of them is a plan tier&lt;/strong&gt;. The&lt;br&gt;
plan decides seats, support and who runs the servers. It does not decide which&lt;br&gt;
of the product you are allowed to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Airtable does that we do not
&lt;/h2&gt;

&lt;p&gt;A comparison post that finds nothing good to say about the other product is an&lt;br&gt;
advertisement, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The ecosystem.&lt;/strong&gt; Airtable's marketplace, its extension gallery and the sheer
number of people who already know it are real advantages, and a smaller
product does not have them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interface Designer&lt;/strong&gt; is mature and has had years of polish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync from other bases&lt;/strong&gt; is a first-class feature there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those are load-bearing for you, that is a reasonable reason to stay.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbioc5nxv438l72aer4m8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbioc5nxv438l72aer4m8.png" alt="A grid view in Appdor, running on your own Postgres" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration, honestly
&lt;/h2&gt;

&lt;p&gt;Your rows will move. Every tool in this category exports CSV and the rows come&lt;br&gt;
out fine.&lt;/p&gt;

&lt;p&gt;What does not come out is the logic — the formula columns are the part that&lt;br&gt;
stalls migrations, because an export gives you the last computed value and loses&lt;br&gt;
the expression that produced it. Appdor reads 7 competitor formula&lt;br&gt;
dialects, Airtable's among them, and recompiles the expression rather than&lt;br&gt;
freezing its output. That is covered properly in its own post.&lt;/p&gt;

&lt;p&gt;It is not a promise that every expression survives. A function with no&lt;br&gt;
counterpart is reported rather than guessed at, and that list of refusals is&lt;br&gt;
your real migration backlog.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://appdor.com/blog/04-appdor-vs-airtable" rel="noopener noreferrer"&gt;appdor.com&lt;/a&gt;. I build Appdor, so weigh accordingly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>airtable</category>
      <category>selfhosted</category>
      <category>database</category>
      <category>nocode</category>
    </item>
    <item>
      <title>What happens when the vendor changes the deal</title>
      <dc:creator>Nebosa Company</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:13:12 +0000</pubDate>
      <link>https://dev.to/nebosa_company_0edc92e18b/what-happens-when-the-vendor-changes-the-deal-2g80</link>
      <guid>https://dev.to/nebosa_company_0edc92e18b/what-happens-when-the-vendor-changes-the-deal-2g80</guid>
      <description>&lt;p&gt;In January 2026 SmartSuite withdrew its free plan. Existing free workspaces kept&lt;br&gt;
working until July, and then they expired.&lt;/p&gt;

&lt;p&gt;I want to be careful here, because this is the kind of story that gets told&lt;br&gt;
unfairly. SmartSuite did nothing dishonest. They gave six months' notice, the&lt;br&gt;
plan was free, and no company owes anyone a free tier forever. Their paid plans&lt;br&gt;
start at $30 per editor per month for Professional and the&lt;br&gt;
product is well regarded.&lt;/p&gt;

&lt;p&gt;The point is not that they behaved badly. The point is that &lt;strong&gt;it was entirely&lt;br&gt;
their decision to make&lt;/strong&gt;, and everyone whose team ran on that plan found out&lt;br&gt;
what that meant on the same day.&lt;/p&gt;
&lt;h2&gt;
  
  
  The thing you are actually buying
&lt;/h2&gt;

&lt;p&gt;When you adopt a hosted work OS you are buying two different things and only one&lt;br&gt;
of them is on the pricing page.&lt;/p&gt;

&lt;p&gt;The first is the software: views, automations, formulas, permissions. That is&lt;br&gt;
what you evaluate.&lt;/p&gt;

&lt;p&gt;The second is a &lt;strong&gt;standing dependency on someone else's commercial decisions&lt;/strong&gt; —&lt;br&gt;
their pricing, their packaging, their roadmap, their acquisition, their&lt;br&gt;
continued existence. That one is not on the page, has no SLA, and is the one&lt;br&gt;
that decides whether the thing you built is still there in three years.&lt;/p&gt;

&lt;p&gt;Nothing about this is unique to SmartSuite. In the last few years this category&lt;br&gt;
has produced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;free tiers withdrawn;&lt;/li&gt;
&lt;li&gt;entry plans repriced;&lt;/li&gt;
&lt;li&gt;features moved up a tier, so the same workflow costs more without changing;&lt;/li&gt;
&lt;li&gt;AI sold as a separate meter on top of the seat you already bought;&lt;/li&gt;
&lt;li&gt;companies acquired, and their products folded into someone else's roadmap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is misconduct. All of it is a vendor doing what a vendor is allowed&lt;br&gt;
to do with a product it owns and hosts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" alt="Appdor in 26 seconds: grid, approval workflow, kanban, dashboard" width="720" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The product tour, silent by design. &lt;a href="https://appdor.com/media/homepage/tour.webm" rel="noopener noreferrer"&gt;Full-resolution video&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What changes if you can run it yourself
&lt;/h2&gt;

&lt;p&gt;Appdor is a work OS on top of a Postgres database rather than inside a vendor's.&lt;br&gt;
The whole install is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.appdor.com/appdor-latest.tar.gz | docker load
curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://get.appdor.com/docker-compose.yml
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; ghcr.io/nebosa-company/appdor generate-keys &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does not make us immune to being a company that makes decisions. We can&lt;br&gt;
change our pricing too. What it changes is &lt;strong&gt;what happens to you if we do&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The database is ordinary Postgres and you can point &lt;code&gt;psql&lt;/code&gt; at it. Your data
does not need our cooperation to be readable.&lt;/li&gt;
&lt;li&gt;The build you self-host is the same build we host, on the same plans. There
is no licence server to call and no activation: a licence is a signed file
checked offline against a key inside the image. Thirty days to try it, then
a licence to keep editing — and reads and exports never lock.&lt;/li&gt;
&lt;li&gt;Nothing is behind a plan. All 48 page elements, 25 chart
types and 24 dashboard widgets ship in every tier, so there is
no ladder for a feature you already use to be moved up.&lt;/li&gt;
&lt;li&gt;If you want to leave, the exit is a &lt;code&gt;pg_dump&lt;/code&gt; rather than a support ticket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ That is a smaller claim than "you are safe", and I would rather make the&lt;br&gt;
smaller one. Self-hosting moves a risk; it does not delete it. You now own&lt;br&gt;
backups, upgrades and uptime. For a lot of teams that trade is not worth it, and&lt;br&gt;
the hosted option exists for exactly that reason.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1frwo50ijole68fcfb3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1frwo50ijole68fcfb3y.png" alt="An approval workflow in Appdor: update record, notify owner, workflow complete" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The question to actually ask
&lt;/h2&gt;

&lt;p&gt;Not "is this vendor trustworthy". They usually are, and it does not help you&lt;br&gt;
anyway — the SmartSuite decision was made by people acting reasonably.&lt;/p&gt;

&lt;p&gt;Ask instead: &lt;strong&gt;if this vendor changed the deal tomorrow, what would it cost us&lt;br&gt;
to say no?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For most hosted tools the honest answer is "a migration we do not have time&lt;br&gt;
for", which is another way of saying the deal cannot be refused. If the answer&lt;br&gt;
is "we keep running the build we already have, on our own hardware, while we&lt;br&gt;
decide" — that is a different position to negotiate from.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://appdor.com/#blog/06-what-happens-when-the-vendor-changes-the-deal" rel="noopener noreferrer"&gt;appdor.com&lt;/a&gt;. I build Appdor, so weigh accordingly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>saas</category>
      <category>nocode</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Your spreadsheet-database can run on hardware you already own</title>
      <dc:creator>Nebosa Company</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:11:14 +0000</pubDate>
      <link>https://dev.to/nebosa_company_0edc92e18b/your-spreadsheet-database-can-run-on-hardware-you-already-own-59hp</link>
      <guid>https://dev.to/nebosa_company_0edc92e18b/your-spreadsheet-database-can-run-on-hardware-you-already-own-59hp</guid>
      <description>&lt;p&gt;Every spreadsheet-database product on the market will tell you your data is&lt;br&gt;
safe. Almost none of them will tell you where it is.&lt;/p&gt;

&lt;p&gt;That distinction stops being philosophical the first time somebody in&lt;br&gt;
procurement asks which sub-processors touch your production records, or the&lt;br&gt;
first time a customer's contract says the row has to stay inside a particular&lt;br&gt;
jurisdiction. At that point "safe" is not an answer. "It is in a Postgres&lt;br&gt;
instance on hardware you control, and here is the connection string" is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f9rxo94vuobgr36k4v1.gif" alt="Appdor in 26 seconds: grid, approval workflow, kanban, dashboard" width="720" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The product tour, silent by design. &lt;a href="https://appdor.com/media/homepage/tour.webm" rel="noopener noreferrer"&gt;Full-resolution video&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What running it yourself actually means here
&lt;/h2&gt;

&lt;p&gt;Appdor is a work OS that sits &lt;strong&gt;on top of&lt;/strong&gt; a Postgres database rather than&lt;br&gt;
inside a vendor's. The database is the system of record, it is ordinary&lt;br&gt;
Postgres, and you can point &lt;code&gt;psql&lt;/code&gt; at it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.appdor.com/appdor-latest.tar.gz | docker load
curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://get.appdor.com/docker-compose.yml
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; ghcr.io/nebosa-company/appdor generate-keys &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole installation. There is no licence server to call and no&lt;br&gt;
activation step: a licence is a signed file, checked against a key that is&lt;br&gt;
already inside the image, so nothing phones home. You get thirty days on the&lt;br&gt;
Business tier to decide; after that the install keeps every read and every&lt;br&gt;
export but stops taking edits until a licence is in. What it never does is&lt;br&gt;
quietly switch a feature off because it noticed it was running outside a&lt;br&gt;
hosted plan.&lt;/p&gt;

&lt;p&gt;The consequences are worth being precise about, because "self-hostable" is a&lt;br&gt;
word that has been used loosely enough to mean nothing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The schema is yours.&lt;/strong&gt; Tables are tables. A migration you write by hand,
a read-replica you attach, a nightly &lt;code&gt;pg_dump&lt;/code&gt; into your own object store —
all of it works, because there is no proprietary storage layer between the
product and the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The product does not phone home.&lt;/strong&gt; No telemetry endpoint and no licence
check. The application and marketing bundles are served from the origin you
deployed — no CDN. One exception, stated because you would otherwise find it
in a network tab: the Map element fetches its basemap tiles from
OpenStreetMap's servers by default, which tells that host a viewer's IP
address and roughly where they are looking. It is one setting
(&lt;code&gt;tileUrl&lt;/code&gt;) pointed at your own tile server, and nothing else changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing is gated.&lt;/strong&gt; All 48 page elements, 25 chart
types and 24 dashboard widgets are present in the build you
just started. There is no plan that unlocks them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbioc5nxv438l72aer4m8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbioc5nxv438l72aer4m8.png" alt="A grid view in Appdor, running from one container on your own Postgres" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that usually breaks
&lt;/h2&gt;

&lt;p&gt;Most migrations off a hosted spreadsheet-database do not fail on the data. They&lt;br&gt;
fail on the &lt;em&gt;logic&lt;/em&gt; — the several hundred formula cells somebody wrote over&lt;br&gt;
three years, in a dialect that only exists inside one vendor's product.&lt;/p&gt;

&lt;p&gt;Appdor reads 7 formula dialects and transpiles them into its own. That&lt;br&gt;
is a separate piece of work with its own article, but it is the reason this&lt;br&gt;
particular migration is finishable rather than merely startable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you should check before believing any of this
&lt;/h2&gt;

&lt;p&gt;Reasonable scepticism about a self-hosting claim looks like this, and all four&lt;br&gt;
are things you can verify yourself in about twenty minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bring it up with the network disconnected after the images are pulled. If it
needs the internet to run, that is where you will find out.&lt;/li&gt;
&lt;li&gt;Connect to the database directly and read a record you created in the UI.
If you cannot, the storage is not really Postgres.&lt;/li&gt;
&lt;li&gt;Grep the built assets for absolute &lt;code&gt;http(s)&lt;/code&gt; URLs. Third-party requests are
not a matter of policy; they are a matter of fact.&lt;/li&gt;
&lt;li&gt;Turn off a feature's network path and see whether the feature admits it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fourth is the one that separates products that are honest from products&lt;br&gt;
that are merely quiet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3vjgui9enbfnf71d6hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3vjgui9enbfnf71d6hx.png" alt="A dashboard in Appdor: counts, activity chart, status donut" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;If you are running a spreadsheet-database today and the answer to "where does&lt;br&gt;
this row live" is a vendor's region list, you have a dependency you did not&lt;br&gt;
choose and cannot audit. Running the same class of tool on your own Postgres&lt;br&gt;
turns that into a deployment decision — which is the kind of problem your&lt;br&gt;
infrastructure team already knows how to have.&lt;/p&gt;

&lt;p&gt;The product is free to run on your own hardware, and it is available in&lt;br&gt;
36 languages.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://appdor.com/#blog/01-self-hosted-airtable-alternative" rel="noopener noreferrer"&gt;appdor.com&lt;/a&gt;. I build Appdor, so weigh accordingly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>postgres</category>
      <category>database</category>
      <category>nocode</category>
    </item>
  </channel>
</rss>
