How it's made

One person, one repository, four products.

The IDE you design in, the runtime underneath it, the player that is the executable of every application you build, the server and the client that pair with it — and this website, the manual and the example projects. All of it out of one folder, written with an AI and decided by a person. This page is the inside story: how it fits together, how that arrangement is kept honest, and two afternoons where the numbers moved.

133,000lines of Swift, in 506 files
31,000of them are tests
4products from one build
1person

Everything is in one place

There is no separate team for the server, none for the website, none for the docs. The repository holds one Xcode project and one Swift package, and everything else is a small tool beside them — the manual is generated, the example projects are generated, the website's pictures are generated, even the application icon is drawn by a program.

That is a constraint, not a boast. One person cannot maintain two of anything. So there is one list component and the data browser draws it too; one printing engine and the quick print from a list goes through it; one shell behind find, sort, import and export. Every place where a second version of something could have appeared, it did not — because there would have been nobody to keep the two in step.

PartWhat it is
RuntimeSQLite, schema, migration, the JavaScript bridge, forms, reports, transfer, the editors
IDEthe designer, the debugger, the build pipeline
Playera small host — the binary of every application you build
Server + Clientthe same payload, one holding the data
Website16 pages, checked by a script
Manualgenerated into the site and into the IDE
Examplesgenerated, never hand-edited

There is no compiler in the build

This is the decision the whole shape rests on. A finished application is our precompiled player — one binary, built and signed by us once — with your project beside it as an encrypted payload. Building copies, encrypts and signs.

Seconds, not minutes

A test build of a real project finishes in about two seconds. The two minutes a release build takes are almost entirely Apple's notarization, not ours.

A fix reaches everything

The runtime is linked by the IDE and by the player. A bug fixed there is fixed in the designer, in Run, in every application rebuilt afterwards, and in the server — at the same moment.

One rule decides where code goes

Anything a running application needs lives in the runtime, never in the IDE. That is why the find window, the import window and the report editor exist inside the applications you ship, not just in the tool.

Written with an AI, decided by a person

Most of this code was written by an AI, working from decisions a person made and under review by that person. That is a plain fact about how Neuridion is built, and it is written here rather than left to be guessed at.

It is also the honest answer to the obvious question — how does one person build an IDE, a database runtime, a client/server layer, a website and a manual? This is how. The interesting half is not that an AI writes code; it is what has to be true for that to be safe.

The person directing it has been building database applications for over forty years. That is the half of this arrangement nobody talks about, and it is the half that decides whether it works. An AI writes quickly and will build, precisely and confidently, whatever it is asked for — including the wrong thing, in a shape that looks entirely reasonable until a customer’s data is in it. What keeps that from happening is not the model. It is knowing, before a line is written, what a data model costs when it is wrong, where a schema migration goes wrong three years later, and why a list must never quietly stop at a thousand rows.

So the instructions are exact, and they are the product. Money is an integer count of minor units, never a decimal. The list reads two hundred rows at a time, however many there are. A rule that only some writers obey is not a rule, so validation lives on the table and not in the form. A built application never checks a licence — not once, not anywhere. Every one of those is a decision made by a person who has seen what the alternative does, handed over as a requirement and not as a suggestion. The machine’s job is to carry it out and to be caught when it does not.

Every claim is checked by a command

The project's own notes say how many tests there are, how many warnings the build has, how many German strings are allowed to remain in an English product. A script compares each number against reality and fails when they differ. A number nobody verifies drifts — three of them did, on one day, before this existed.

A new test has to be proven to bite

After a fix, the fix is deliberately broken again to watch the new test go red, then put back. Twice a test written after a bug would have passed with the bug still in place. Once — this week — a test looked right and proved nothing, because the two names it compared happened to sort correctly anyway.

Nothing is deviated from quietly

Decisions are written down with their date and their reason. If something turns out to be impossible, the work stops and the person is told — instead of a slightly different thing being built and never mentioned.

Measure, do not guess

When a diagnosis is a guess, the answer is a spike: the smallest possible program that settles it. Both stories below started that way, and one of them overturned what everybody assumed the problem was.

What that does not mean. It does not mean nobody reads this code — every line is reviewed, and the decisions in it are a person's. It does not mean fewer tests; it means more, because tests are how a machine's work is held to account. And it does not mean the mistakes below were avoided: they are all real, they were all found here, and they are why the guard rails exist in the shape they do.

And this is the instrument the reading is done with

Reviewing a machine that writes hundreds of lines an hour has one real difficulty, and it is not reading code. It is knowing what changed — which types are new, which function quietly grew a parameter, which file was touched for a reason nobody wrote down. So the same person wrote a second program for it. Sextant parses this repository — 506 files, 1,111 types, 4,534 functions, in under three seconds — and watches it while the agent works. It reads Swift, which is what Neuridion is written in.

Sextant showing a live activity window: an AI agent is working right now, five
                edits across three files, with every symbol it added or changed listed by name
                and file
Taken while the code on this page was being written. Every symbol the agent had just added, changed or removed — by name, with its file. The bar above it is the worklist: how many of those changes touched a risk zone, how many are signed off, and how many are still open. Here, one and one — the work was not finished yet.

That screenshot is not a mock-up and not a re-enactment: it is this project, that evening, while the buttons beside a list were learning to sit down the side of it. RowActionEdge, railGroup, rowActionRailWidth, setActions(_:edge:handler:) — the review starts from that list rather than from a diff of four hundred lines, and the difference is whether a person can actually read a day’s work or only claim to.

Six lenses ask six questions, and each answers with places in the source rather than with a verdict — the rule for that window is that every row must be openable at a line, and what cannot be pointed at does not belong in it. Four of them matter to anybody who has to trust what came out of this workshop:

Everything this source touches outside itself

Every network address, every keychain call, every settings key, every folder, the pasteboard — and credentials, recognised by a provider’s token format, by the name they are assigned to, or by shape alone, and always shown masked. What is not in that list, the application does not talk to. It is the check behind our privacy page, and that claim is worthless unless something can enumerate it.

Names that point nowhere

An invented symbol name, an image that is not in the asset catalog, a text key with no entry — all of them compile, and all of them fail on screen. With a rule we would happily steal for anything: no catalog means no claim. A kind that could not be checked is listed as unchecked, because a missing catalog must never look like a clean result.

Started and never stopped

Timers, observers, file watchers, sensors, audio — and tasks whose handle nobody kept, so nothing can ever cancel them. Balanced per kind rather than per function, so a timer begun when a window opens and ended when it closes is not a complaint. This is the family of fault that costs nothing at all on the machine it was written on.

The same thing written twice

Functions with the same control-flow shape and the same call order, whatever they are named — exactly what copy-and-rename leaves behind. A fact written twice is a fact that will be updated once, which is this project’s most persistent bug; here it is countable instead of remembered.

And the part that is easiest to miss is the one that matters most: a review is a state, and it expires by itself. Every symbol carries a human verdict — under discussion, needs redesign, verified, signed off — and that verdict is pinned to the shape the symbol had when it was given. Change the shape and the sign-off flips back to outdated on its own. That is the difference between a review and the memory of one: nobody has to remember which of four and a half thousand functions was read before the last rewrite went through it.

What Sextant does not do is decide. It reports; the person decides, and the person is the one who then tells the agent it got something wrong. That order matters more than any single check in the list: a review tool that quietly fixes things is a second author, not a review.

Sextant's inventory listing every network address, keychain call and settings
                  key the source contains
Every address in the source, listed.
Sextant's duplicates lens: 43 groups of functions that share the same
                  control-flow skeleton
Forty-three places where one thing is written twice.
Sextant is its own product, and it runs entirely on this Mac. The source is read locally — no upload, no cloud index, no telemetry — and the snapshots it compares against are AES-256 encrypted inside its own app container, with the key in the keychain; with Apple Intelligence its optional AI runs on the device too. It is written by the same person, in the same workshop, for a job that had no tool yet, and it is on its way to the App Store. We mention it here because the sentence above — every line is reviewed — is easy to write and hard to do, and this is how it is actually done.

A story

The attachment nobody was looking at — and how a database became two files

Neuridion stores a scanned invoice in the record itself: no media folder beside the data, nothing to lose when somebody moves a directory. The question is what that costs when a list shows two hundred rows and nobody wants to see a single scan.

The answer, for a long time, was everything. Reading records compiled to SELECT table.*, so every row arrived with its documents attached, whether or not anybody looked. The laziness everyone believed in was one floor too high: it sat at the bridge — a script was carefully handed an object without the bytes — while the bytes had already been read from disk, decrypted, and put in memory to build that object.

A spike settled it: three hundred rows, a five-megabyte scan on each, a database of 1.5 gigabytes, read once the old way and once with file columns left out of the query.

Reading 300 recordsBeforeAfterFaster by
Plain database0.376 s0.001 s376×
Encrypted (SQLCipher)1.725 s0.002 s862×
1 ms 10 ms 100 ms 1 s plain encrypted Logarithmic — each dashed line is ten times the one before it. 0.376 s 0.001 s 1.725 s 0.002 s
The bars are on a logarithmic scale, because on a linear one the two fast bars would be invisible — which is rather the point. And the comparison is conservative: the old figure does not include building the records that the new figure does.

Why the database is now two files

Making the read lazy raised the next question: where do those bytes live? Leaving them in the row means every page SQLite touches drags megabytes past the disk head. So the documents moved into a sibling database — Customers.files.sqlite beside Customers.sqlite — attached to the same connection with the same key.

One transaction, still

Attached to the same connection, a write that touches a record and its document is one atomic transaction. Two files, one promise.

Encryption comes free

The sibling uses the same key. There is no second thing to configure and no way to end up with an encrypted database and plaintext scans.

The list never opens it

The record's own column keeps the envelope — name, kind, dates, size — so a list draws “Contract.pdf · 2.4 MB” without touching the second file at all.

No file fields? No second file

A project without documents has exactly one database, as it always did.

The idea that was designed and then thrown away. Storing each document under the hash of its content would have deduplicated identical files — and made changing a document a birth: a new row, an old row that is now rubbish, and therefore a garbage collector, and therefore a reference table to know what is still in use. One-to-one ownership instead: replacing a document is an update, deleting one is immediate, and there is nothing to collect. The price is no deduplication, and it is deliberate.
And the trap it set. SQLCipher's export function defaults its source to the main database. Written the short way, a backup would have copied every record and silently left every document behind — verification passes, and you find out on the day you need the backup. Four tests now stand in front of that one line.

Another story

We tortured the server until it stopped answering

A MacBook Air as the server, a MacBook Pro loading it over a gigabit cable with real paired clients — lists of 50 and 200 rows, counts, a sum, a sum grouped over a relationship, and a create·save·delete in every ten. Each client offers about ten requests a second: a busy desk, not a stopwatch. Twenty steps, up to four hundred clients at once, against Harbour Supply’s 7,600 invoices.

Concurrent clientsRequests / secondTypical answer
198.6 ms
201895 ms
605694 ms
1009326 ms
2001,19866 ms
2401,22595 ms
3001,204148 ms
4001,166242 ms

Not one error, and not one refused connection, anywhere in that climb — over the cable, and again over Wi-Fi. It gets slow and it does not fall over, which is the property that matters: a server that goes sluggish under overload is one you may overload. One that throws errors is downtime.

1400 1050 700 350 1 20 60 100 200 240 300 400 1,225 / s
The spacing is the staircase of the run, not a linear axis. Up to a hundred clients the server answers inside ten milliseconds; past two hundred it is full and everything after that is queue. Where somebody stops enjoying it is a different number from where it breaks — the developer, working in an extra client throughout, called 200 the limit for pleasant work, waited two seconds a click at 240, and was still getting answers at 350.

And the database was never the reason

The server answers every command on one serial lane, by design: SQLite has a single writer, and the order of table events is business logic. The obvious guess is that the lane is what fills up. We measured it, from inside the lane itself — and it is not. It reaches 80 % at 140 clients and then stops climbing, while the wait quadruples. At 1,225 requests a second that is 0.6 milliseconds of database work per request, against the 242 milliseconds a client waits at four hundred. The network costs 1.3. Everything else is queueing above the database.

That is worth more than the headline number, because it says what to build next — and it is not a faster database.

Over Wi-Fi, one user is slower than two hundred

The same run over Wi-Fi 6 gave the result nobody expects. A single client waited 83 milliseconds where the cable took 8.6 — ten times worse. At eighty clients it was 38 milliseconds, and at four hundred Wi-Fi beat the cable outright. The radio sleeps between packets, and every wake costs tens of milliseconds; a continuous stream keeps it awake. The worst Wi-Fi load is a quiet office, not a full one. The link was a good one — channel 36, 33 dB of headroom — which is what makes this a fact about power saving rather than about somebody’s flat.

And then it stopped accepting anybody, for a reason nothing could see

The run ended badly: past four hundred clients the server began refusing connections, and it did not come back on its own — the clean-up pass minutes later could not get in either. The obvious reading was that four hundred is simply the limit. It was not. The process was holding 8,169 TCP sockets — 7,871 of them already closed — against roughly 8,200 connections it had ever accepted. Every single one.

Everything visible had been right. When a client hangs up, the server reaps the bookkeeping: the connection table, the statistics, the locks it held. What nobody ever did was tell the networking layer it was finished with the connection, and until you do, it keeps the file descriptor. In ordinary use that is one lost descriptor per client that ever connects — every restart, every wake from sleep — until a server that has been up for weeks quietly admits nobody — and in a load test it looks exactly like a capacity limit, which is how it nearly became one in this very story.

It is one line. The test that keeps it away counts the sockets on the transport’s own port, because the connection table was correct the whole time the sockets piled up: anything that asked the server how many clients it had would have passed with the bug in place.

What the whole exercise bought, with the leak repaired: zero failed requests and zero refused connections through four hundred clients, on cable and over Wi-Fi. And the behavior that matters more than any number — it slows down, and it does not fall over.

Measured, then decided

Five decisions, and the numbers behind them

None of these were taste. Each one started as a measurement, and two of them ended somewhere other than where they were heading.

A million rows, and only what is on screen is read

A list used to fetch the first thousand records and say nothing about the rest — the quiet kind of wrong answer, because it looks like a complete one. The list is an AppKit table now, and it reads a page of 200 rows at a time, keeping at most eight of them. Scroll to row 47,312 and that page is fetched; the pages you scrolled past are dropped once they are more than four away. At the very most about 1,600 records are in memory, whatever the table holds.

The row count itself is not a fetch at all — it is a count(*), which SQLite answers without reading a single row. The stress-test project has 1.01 million rows across 160 tables; opening it takes 0.11 s, and scrolling its biggest table is the same work as scrolling a table of fifty.

Totals are the database's job

A footer under a money column sums the whole set — all 7,628 invoices — not the rows that happen to be loaded. A limit nobody can see is a wrong answer wearing the face of a right one, so the sum is a query and never a loop.

Charts too

Grouping for a chart is done in SQL — the example's dashboard says so out loud: “Grouped in SQL — no records are loaded.” Six bars over seven thousand invoices cost one statement.

A relationship stays a query

Until somebody treats it as a list. invoice.lines.count() counts in the database; asking a customer with four thousand invoices how many they have must not load four thousand records.

Documents stay on disk

The story further up this page: a row's scans are not read until something asks for them. That is the same rule one floor down — read what is shown, not what exists.

And the decision that came with it: the old list was deleted, not kept beside the new one. One list component, used by the forms you design, by the data browser, and by the preview inside the find and import windows — because a second one would be a second set of bugs and nobody here to keep the two in step.

Money is an integer

In floating point, 1.005 × 100 is not 100.5. It is 100.49999999999999, and rounding that gives 1.00 € where 1.01 € was owed.

While amounts are only typed in, that is a curiosity. The moment a line total, a tax amount and an invoice total are computed, every line of every invoice runs through that one rounding — and a curiosity becomes an accounting error that is very hard to argue with afterwards.

The decision

An Amount is a whole number of minor units — cents — from the moment it is typed to the moment it is printed. Never a floating-point number, anywhere, at any step.

Its own decimal places

The field says how many, not the currency's habits: two for the euro, three for a dinar, none for the yen. And a money field carries its currency wherever it is drawn, so a list cannot show a bare number where a form shows money.

Sums in C, over integers

Totals are added by SQLite, over whole numbers. Which is why the total on a form and the total on the printed report cannot disagree — they are the same statement, not two additions that happen to be near each other.

Nailed down by a test

1.005 and 2.675 are the two classic cases where floating point quietly rounds the wrong way. Both are in a test, and the test was watched failing against the old code before the fix was believed.

Three more, in short

The wire: JSON was the bottleneck

Before a line of the server existed, a spike measured the protocol — and encoding cost more than the network did. Decision: a hand-written binary format for rows, and JSON never for row data. Today a 500-row list is one trip at 5.8 ms; one record is 0.13 ms; a ping is 0.09 ms.

Ten backups of 3 GB cost 3 GB

Scans rarely change, so the attachment store counts its own writes: two backups carrying the same number carry the same documents. Decision: clone on APFS rather than copy — a genuinely separate file that shares its blocks. A hard link was rejected: SQLite writes to a database merely by opening it, so one backup could damage another.

We measured what batching would buy — and did not build it

Over the wire each save is its own round trip. Measured: 4,811 against 25,550 rows a second, 5.3× and 86 % of what is possible with no network at all. Decision: wait. Three questions are unanswered — a refusal would arrive after the loop, a read inside the transaction would not see its own unwritten rows, and deleting has no bulk command. The number is on the list beside the question, so the decision is made with it rather than without it.

And at the end

What the editor sees before anything runs

A script that has not been run yet is still a script that can be read. The editor knows the data model, so a field name that does not exist is underlined while it is typed — and the window it belongs to carries a mark in the sidebar, so you find it without opening every file.

The code editor with a misspelt field name underlined in red, and the window
                it belongs to marked with a warning triangle in the sidebar
A typo in a field name, found without running anything — and the window marked in the sidebar.
The mistake that costs the most is the silent one

Writing to a field that does not exist used to be accepted without a word: the value went into an ordinary JavaScript property and was saved nowhere. A crash is seen the same afternoon; that is seen in three months, by somebody asking why a column is empty. It is refused now — and underlined before it is ever run.

A warning that cannot be reached is a to-do list

Every place the checker complains about carries its file and its line, and every line in the console is a button that opens the script there. The reason is unglamorous: the alternative is somebody hunting through tabs, and a warning that costs a hunt gets ignored the third time.

And the mark goes when the fault does

Fix the name and the underline is gone; the sidebar’s triangle goes with it. That sounds obvious and was not: the mark used to survive every edit until the script happened to run again, so a repaired fault looked exactly like an unrepaired one.

The editor underlining a bare database.invoices.all(), with a popover saying
                that invoices holds 7,628 rows in the development database
The same idea one step further: this name is spelt perfectly. The editor has the development database open, counted the rows, and says the number — and the three .where(…) calls higher up the same script are left alone, because somebody has already thought about those.
The first version of that warning lasted four hours. It fired at every .all(), hit nine places in our own example project — and every one of them was right: two hundred customers are loaded whole precisely so that twenty-six thousand invoice lines are not. From the source alone, “read 200” and “read 200,000” are the same three words. A checker that underlines correct code is one nobody reads twice, so it came out again the same day. It speaks now only when it has measured, above five thousand rows, with the real number in the message — and the way out it recommends is one the product actually has. An earlier draft of that very sentence suggested a command Neuridion has never had.
The same script after the fix: no underline, and the sidebar shows an unsaved
                change dot instead of the warning triangle
The same file one word later: nothing underlined, no triangle — just the dot that says this is not saved yet.

The tests, and the commands that keep the story honest

1,519tests, every release
200test files
0warnings tolerated in the IDE
1command runs the lot
Every feature arrives with its own tests

Not as a rule somebody remembers — as the way the work is done. Sound arrived with ten tests, the update check with thirteen, computed fields with six suites. The tests are what makes the next change safe, which is the only thing that matters to somebody already using the version before it.

A fix is not finished until its test fails without it

After a bug is fixed, the fix is deliberately broken again to watch the new test go red. Twice a test written after a fix would have passed with the bug still in place — which is a test that proves nothing and reassures everybody.

Two drawings of one object are counted, not eyeballed

The designer canvas and the running window are separate renderers. A test counts the settings each one reads: anything the window honors and the canvas ignores fails the build. Four rounds of reading the two files side by side had missed what counting found in seconds.

Every script of every example is checked

One command runs all ~85 scripts of the shipped projects through the real engine: does it parse, does every table and field it names exist, can the debugger rewrite it. The first time it ran it found fifty-five complaints — every one of them a bug in the checker, under correct code.

The interface is tested by driving it

Some faults exist only while the program is running — a keystroke that never reaches its handler, a sheet that swallows every click. Those are found by a test that opens the application and uses it, not by reading the source.

The pictures on this site are taken by the machine

They regenerate with one command, so a picture cannot quietly go on showing a version that no longer exists — which is exactly what had happened to the set before them. A second command checks that every picture a page names is actually there: it found a chapter of the manual pointing at a file nobody copied, in the shipped product, with nothing failing over it.

The lesson that cost the most time

A test that measures the machine is worse than no test. Four of them wobbled under load — and three had one cause, which was not the clock: the thing they waited for is delivered on the main queue, which an application services constantly and a test process services only by luck. They passed on an idle Mac and failed while something else was compiling. The cure was an injectable delivery queue and waiting for a state instead of a duration; the proof is the suite run three times with four processes deliberately burning CPU.

What this does not claim. Tests do not make software correct; they make it hold still. The honest promise is narrower and more useful: what worked in the version you have will still work in the next one, and where something does break there is a failing test with your name on it rather than a shrug. That is the whole reason to accept an update from a one-person product at all.

The point of all of it

This tool is not made fast by a compiler. It is made fast by the shape of the data.

Every number on this page comes from the same kind of decision, and none of them came from tuning the language. They came from asking the program to do less.

What made it fasterFactorWhere it came from
Compiling the hot code20×JavaScriptCore, for free, while your application runs
Not reading documents nobody asked for376×a decision about what a query names
The same, on an encrypted database862×the same decision — every byte not read is also a byte not decrypted
Sending a batch instead of a round trip5.3×measured, and deliberately still on the list

A compiler gives you a constant factor, and we take it gladly: SQLite is compiled C, JavaScriptCore turns your hot scripts into machine code, the runtime underneath is compiled Swift. But the compiler was never the interesting part. Twenty times faster is what a compiler buys. Eight hundred times faster is what it buys you to not read three hundred scans off a disk because somebody opened a list.

That is why the work went where it did: a page of 200 rows instead of a thousand-row cap, a count(*) instead of a fetch, a sum computed by the database instead of a loop, a grouped chart that loads no records at all, documents in a sibling file that a list never opens, and a wire that sends five hundred rows in one trip. None of it is clever code. All of it is a decision about what is not done.

And the part that matters to you: none of this is yours to tune. There is no query planner to argue with, no index to guess at, no lazy-loading flag to remember, no “fetch strategy” on a form. You drag a list onto a window and it pages, counts and totals correctly over a million rows — because the decision was made once, down here, rather than handed to every developer as an option they have to get right. Designing stays the fast, simple part. That is the whole point of the thing.