For engineers

Most AI builders write code. Ours configures an engine.

The logic in your app is data: a config the engine checks before it runs, on the same engine every app runs on. Below: the architecture, a real config, the guarantees and where they stop, and the license, said precisely.

book_class.json
{  "id": "book_class",  "exec": {    "v": 1,    "on": { "trigger": "form_submit" },    "steps": [      { "id": "book", "action": "transact", "steps": [        { "id": "seat", "action": "update_record",          "collection": "classes",          "target": { "from": "input", "ref": "class_id" },          "incs": { "seats": -1 },          "guard": { "field": "seats", "op": "gte", "value": 1 },          "guardMessage": "Sold out. No seats left." },        { "id": "booking", "action": "save_record",          "collection": "bookings",          "fields": {            "class": { "from": "input", "ref": "class_id" },            "name":  { "from": "input", "ref": "name" },            "email": { "from": "input", "ref": "email" } } }      ] },      { "id": "confirm", "action": "email",        "to": { "from": "input", "ref": "email" },        "autoReply": true }    ]  }}
validateExecConfig → ok
7 triggers · 47 actionsthe whole vocabulary, listed below
Checked 3 timesat build, at save, before every run
docker compose upthe export runs without us
FSL-1.1-MITeach release turns MIT after 2 years
How it runs

The AI writes pages and configs. One engine runs them all.

There is no generated backend per app. The AI writes your pages, real HTML and CSS in your brand, and describes what should happen as data. One engine, the same code for every app, checks that data and runs it against Postgres.

The AI writesper app
pages/*.html

Real HTML and CSS. Brand colours by variable, so a palette change re-themes every page.

behaviors (JSON)

A trigger and up to 20 steps, with bindings to the form, the record or the member. Data, not code.

The validator
at build
at save
before every run

Unknown actions, unsafe shapes and prices taken from a form are refused.

One engine runsevery app
Postgres

Records as JSON documents, row-level security forced on.

Email · push · payments

Sent from the steps; payments on the owner’s own Stripe account.

Your JavaScript (optional)

The escape hatch, in a sandbox. Section 08.

Checkable before it runs.

A config can be validated. A generated codebase can only be read, by someone, later.

Fixed once, fixed everywhere.

Every app runs the same engine, so a fix or a new capability lands for all of them at once.

Less to go wrong.

A feature is a page of config, not a new service. Fewer generated lines, fewer places to break.

One feature, traced

One feature, all the way down.

Book a seat in a class, never sell the same seat twice, confirm by email. This is the whole backend for it, and what Postgres does with it.

book_class.json
{  "id": "book_class",  "exec": {    "v": 1,1    "on": { "trigger": "form_submit" },    "steps": [      { "id": "book", "action": "transact", "steps": [2        { "id": "seat", "action": "update_record",          "collection": "classes",          "target": { "from": "input", "ref": "class_id" },          "incs": { "seats": -1 },          "guard": { "field": "seats", "op": "gte", "value": 1 },3          "guardMessage": "Sold out. No seats left." },        { "id": "booking", "action": "save_record",          "collection": "bookings",          "fields": {            "class": { "from": "input", "ref": "class_id" },            "name":  { "from": "input", "ref": "name" },            "email": { "from": "input", "ref": "email" } } }      ] },      { "id": "confirm", "action": "email",4        "to": { "from": "input", "ref": "email" },        "autoReply": true }    ]  }}
what Postgres runs · abridged
UPDATE app_data   SET data = jsonb_set(data, ARRAY[$1],              to_jsonb((data->>$1)::numeric + $2)) WHERE app_id = $3 AND env = $4   AND collection = $5 AND id = $6   AND (data->>$7)::numeric >= $8   -- the guard-- 1 row  = seat taken, carry on-- 0 rows = stop: "Sold out. No seats left."
1
Checked before it runs.

Shape, trigger, every action and binding. This exact config: ok.

2
One run per submit.

Each submit carries a run id. A retry of the same submit claims the same id and does nothing (ON CONFLICT DO NOTHING).

3
The seat and the booking commit together, or not at all.

The guard is inside the UPDATE’s WHERE clause. Two people, one seat: the second update matches 0 rows, the transaction rolls back, and they see “Sold out.”

4
Then the confirmation.

An auto-reply to the address on the form, from a fixed template, capped per app per day.

The vocabulary

Every word the engine knows. All of them.

A behavior is one trigger and up to 20 steps, and the routines it calls can take one run to 100. These are all the triggers and all the actions there are. Anything else is refused by the validator, or written as your own code in a sandbox (section 08).

7 triggersform_submitrecord_createdschedulepayment_succeededsms_receivedwebhook_receivedadmin_action

Data8

save_recordget_recordlist_recordsaggregate_recordsupdate_recorddelete_recordtransactfor_each

Money7

chargesubscribeseller_onboardnative_purchaserequire_entitlementspend_creditsgrant_credits

Accounts10

create_userlog_inlog_outupdate_memberupdate_credentialsdelete_accountsend_password_resetsend_magic_linksend_confirmation_emailrequire_role

On screen8

redirectshow_toastcomplete_formscroll_toshare_sheethapticset_badgeprime_push

Reach4

emailsend_smspush_notificationcall_api

Time3

scheduleschedule_recurringcancel_schedule

Control3

stop_runrun_custom_eventrun_isolate

AI1

ai_generate

Files2

upload_filedelete_file

Compute1

compute

Reserved3

conditiongoto_pagesocial_auth
compute · 18 opsadd · subtract · multiply · divide · round · percent_of · concat · upper · lower · trim · now · date_add_days · date_diff_days · date_format · random_code · format_currency · choose · distance_km
phone app onlybuilt, not switched on yet (texting waits on carrier registration)reserved, not runnable
Guarantees

What it guarantees. And where that stops.

Each promise below is a mechanism in the runtime. The third column is where that mechanism ends.

GuaranteeHowWhere it stops
Check-and-write is one stepThe guard is part of the UPDATE’s WHERE clause, every value a bound parameter. 0 rows means the run stops with your message.One record, one incremented field per guarded update.
Several writes, all or nothingtransact runs 2 to 6 saves, updates or deletes in one Postgres transaction. A failed guard rolls the group back.Steps outside a transact run in order; a failed step is logged and the run carries on.
A retry doesn’t run twiceEach submit carries a run id: primary key, ON CONFLICT DO NOTHING. Payments carry an idempotency key to Stripe.At-most-once: if a run dies halfway, the retry won’t finish it. Not exactly-once.
Follow-ups fire if the record existsThe follow-up is written to an outbox in the same transaction as the record. Workers claim it with FOR UPDATE SKIP LOCKED and retry up to 8 times; run ids are deterministic.Chains stop 3 deep, so one behavior can’t trigger itself forever.
Work is bounded20 steps per behavior, 100 once routines expand, loops of 25 rows × 5 steps, routines 5 deep.Fixed limits. Large batch jobs belong somewhere else.
An AI step can’t stall a runTyped output (up to 12 fields), a fallback value, and a spend cap per run ($0.50 by default).When the provider is down or the cap is hit, the fallback is what runs.
Configs are checked, three timesAt build, when you save, and again before every run. Unknown actions, unsafe shapes and prices taken from a form are refused.A missing collection is a warning, not a refusal.
The builder

The AI that builds your app holds no keys.

It has no database credentials, no shell and no web access, and none of its tools can name an app, a user or a job. What it hands back is checked by machines, then used by a second AI that tries to break it.

In the room

ReadEditWriteGlobGreppage files in its own workspacepropose: behaviors · data types · admin · brand · plugins

Everything it proposes beyond pages (behaviors, data types, plugins) is re-checked by the server before it lands.

Not in the room

database credentialsa shellweb fetch or searchsub-agentsapp, user or job ids

It refuses to start if a database URL is present. Its tools take no app, user or job id, so it can’t ask for anyone else’s.

1
Ship gates

Deterministic checks: every form wired to a behavior that exists, input names that match their bindings, no hand-written data calls, brand colours by variable, text contrast at 4.5:1. One chance to self-correct, then it stops.

2
The server’s own validator

The same validator that will receive the build, run as a dry run while the builder can still fix things. It keeps going while the error changes and stops when the same error repeats.

3
A second AI uses it

Opens the app at phone size (390 × 844), clicks, types, submits, checks the record in your Admin and the email that would have gone out. It must file a verdict; no verdict counts as a fail.

4
An immutable draft

Every build is a saved version. Nothing reaches your live app yet.

5
You publish

Publishing moves one pointer to that version; restoring moves it back. Every move is logged.

Your app builds one change at a time, so two builds can’t race; other people’s apps build in parallel. On a fixed-price build, a failed verdict refunds itself.

Security

Walls in the database, not only in our code.

Owner, member and public reads run under Postgres row-level security, forced on and failing closed when the tenant isn’t set. The engine’s own steps run with explicit app and member filters. Secrets, outbound calls and money each get a wall of their own.

Tenants

Row-level security, forced

Forced on the tenant tables. The tenant is set per transaction; unset means zero rows, not every row.

Members

Two keys to one row

A member’s read must match both the owner and the member. Credits and entitlements are system-write only: nobody writes their own balance.

Secrets

No way to read them back

Envelope encryption: a per-app key (HKDF), a per-secret AES-256-GCM key, the app id bound in. There is no read API. Every use writes an audit row first, or doesn’t happen.

Outbound calls

Declared hosts only

https on 443 to endpoints the owner declared. DNS resolved twice and pinned; private and metadata addresses blocked; every redirect re-checked, 3 at most.

Money

The truth comes from the source

Payments run on the owner’s own Stripe account, webhooks signature-checked. An App Store or Google Play notice is a doorbell: we re-fetch the truth with the owner’s credentials.

Logins

Nothing useful in a dump

bcrypt at cost 12. Session tokens stored only as SHA-256 hashes. Locked for 15 minutes after 5 failed tries.

143/143probes passing · run 2026-06-27

The day our own policy leaked.

Before launch we pointed a 143-probe adversarial battery at our tenant walls. It found a real cross-tenant read in our first policy: an uncorrelated subquery on site versions. We fixed the policy and kept the battery. It passes every probe on production.

Custom code

When the vocabulary runs out, write JavaScript.

Custom code ships inside a plugin manifest: the services it may call, the secrets it needs, what it may do with data. The platform checks the manifest, then runs the code in an isolate with no network of its own.

plugin.jsonvalidatePluginManifest → ok
{  "id": "mailchimp_sync",  "name": "Mailchimp sync",  "category": "marketing",  "version": 1,  "secrets": [{ "key": "api_key", "label": "Mailchimp API key" }],  "egress": [{ "id": "mc", "host": "us21.api.mailchimp.com",               "pathPrefix": "/3.0/" }],  "provides": { "customEvents": { "add_subscriber": {    "signature": [{ "name": "email", "required": true }],    "body": { "steps": [{ "id": "run", "action": "run_isolate",      "pluginId": "mailchimp_sync",      "args": { "email": { "from": "args", "ref": "email" } } }] } } } },  "worker": {    "scopes": ["egress:request", "log:write"],    "code": "async function handler(input, ctx) { … }"  }}
1
The isolate

Its own V8 isolate with no bindings at all: no database, no keys, no network. 100 ms of CPU, 20 subrequests, 512 KB of code.

2
The outbound wall

Every fetch() it makes goes to one place, our mediator. Any other address is refused.

3
The mediator

Checks a token signed for this one app, good for 120 seconds, and the scopes granted right now. Four exist: read data, write data, call an endpoint, log.

4
The egress broker

Only hosts the owner declared, https on 443, DNS pinned, private addresses blocked. The secret is attached after those checks and scrubbed from the reply.

no paymentsno schedulingno other app’s data
FirstReuse

An integration already installed in your app.

ThenInstall

One from the shared library.

LastWrite

A new plugin. Custom code is the last rung.

It goes live with the build, no review queue. Change the code or its scopes and it’s re-approved from scratch. A person looks at it only when it’s promoted to the shared library for everyone.

Export + license

Leave whenever you like. The engine comes too.

One export is a repo that runs anywhere Docker runs: your pages, your data, the runtime and the database setup, row-level security included.

steadbrew/
app.jsonevery page, behavior and collection
index.jsstarts the runtime
package.json@lynqly/runtime, nodemailer
migrate.jstables and row-level security
seed.jsloads your data
seed/records.jsonevery record
seed/members.jsonyour members (git-ignored)
public/*.htmlyour pages, SEO tags baked in
.env.examplesecret slots by name, never values
Dockerfilenode:20-slim
docker-compose.ymlthe app and postgres:16
README.md · TRUTH.mdwhat’s here and how to run it
your machine
$ unzip steadbrew.zip && cd steadbrew
$ docker compose up
# runs: node migrate.js && node seed.js && node index.js
→ http://localhost:3000
The license, said precisely

FSL-1.1-MIT. MIT on a timer.

Use it, read it, change it, run it for your business. The one thing it doesn’t allow is using it to offer a competing product. Two years after each release, that release becomes plain MIT. We call it Fair Source, never open source, because for its first two years it isn’t.

How you get it

An encrypted zip and a one-time link that expires in 15 minutes, after you re-enter your password. On every plan.

Your members’ logins

Tick one box at export and their password hashes travel with the data. Leave it off and they set a new password by email. Either way, everyone signs in once more.

What stays behind

Custom-code bundles, payment and activity history, and scheduled jobs that haven’t run yet.

Limits

What it won’t do. On purpose, and not yet.

Every architecture is a set of trade-offs. These are ours, and the edges that aren’t built yet, so you find them here and not in week three.

On purpose

A finite vocabulary

47 actions. Past them is your own JavaScript in a sandbox, or it isn’t built yet. That limit is what makes every config checkable.

Pages are plain HTML and CSS

No framework to eject from, and no React app to hand-edit either. Anything a browser runs, your pages can be.

One data shape

Records are JSON documents in Postgres. Field types are declared, not enforced on write; validate in the behavior when it matters.

Bounded runs

20 steps per behavior, 100 per run, loops of 25 rows, chains 3 deep. Heavy batch work belongs somewhere else.

One build at a time per app

Changes to the same app queue up, so two builds never race.

Not yet

reservedSocial sign-in
reservedBranching and page jumps as their own steps (condition, goto_page)
waiting on carriersTexting
not yetCustom code inside an export
not yetA public API and keys for your app’s data
Where this sits

Three ways to build with AI. Pick by architecture.

Each approach is a real choice with a real cost. Here is how they differ underneath, our column included.

Code generatorsClosed buildersAn engine you can take
What the AI makesA codebaseConfigs on their runtimeConfigs on an engine you can export
Who maintains itYou, every line, foreverThe vendorThe engine: one fix reaches every app
Checked before it runsIf someone reviews itBy the vendor’s rulesBy a validator, three times
Concurrency and moneyWhatever was generatedTheir primitivesGuards, transactions, pinned prices
Runs without the vendorYes, it’s your codeNoYes: docker compose up
Where it shinesAnything is possibleMature, lots of pluginsCheckable, portable, one engine
What it costs youYou own every bugYou can’t leaveA finite vocabulary (custom code covers the rest)
Questions

Good questions. Before you trust it.

Is this a wrapper around a language model?No
No. The AI writes your pages and configs while you build. At run time your app runs on a deterministic engine with no model in the loop, unless a step asks for one (ai_generate), and that step is typed, capped per run and has a fallback.
Why configs instead of generated code?Fact
A config can be checked before it runs; generated code can only be read. Every app runs the same engine, so one fix reaches all of them. When you need something the vocabulary can’t say, you write JavaScript in a sandbox.
Can I read the engine’s source?Yes
Yes. The runtime ships as @lynqly/runtime under FSL-1.1-MIT, and every export includes it.
Is it open source?No
No. It’s Fair Source: FSL-1.1-MIT. Use it, read it, change it, run it for your business; don’t use it to offer a competing product. Each release becomes MIT two years after it ships.
What happens to my app if Lynqly disappears?Fact
Export it and run docker compose up: your pages, data, members and the runtime, on your own machine. And two years after each release, that release is MIT.
Is anything exactly-once?No
No. A submit runs at most once: one run id per submit, so a retry is a no-op. Follow-ups go through an outbox written in the record’s own transaction, retried up to 8 times with deterministic run ids.
How are customers kept apart?Fact
Owner, member and public reads run under Postgres row-level security, forced on and failing closed; the engine’s own steps filter by app and member explicitly. A 143-probe adversarial battery checks the walls.
Does the AI see my customers’ data?Fact
The builder has no database access at all. An ai_generate step sees only what its config hands it, and its usage receipts never store visitor data.
Which AI models do you use?Fact
Steps name a level: fast, smart or frontier. We choose the model behind each level and move it as better ones ship, so your config never changes.
What can’t it express?Fact
Anything outside the 47 actions: then it’s your own JavaScript in a sandbox, or not built yet. The limits section lists the edges.
Where does my data live?Fact
Postgres 17 behind PgBouncer, in a database separate from the platform’s own, as JSON documents with row-level security.
Can I call my app from my own code?No
Not with an API key yet. Other services can call into your behaviors through a webhook trigger today.
Still have a question? A real person answers.✉ hello@lynqly.comHelp Center →
Your turn

Build something small. Then read what comes out.

Every behavior it writes is a config you can open. Every app can leave with you. Start with a plan; it’s free.

Start building →

Free to plan · export on every plan

Lynqly© 2026 Lynqly · an engine you can take with you
@lynqly/runtime is FSL-1.1-MIT: each release becomes MIT two years after it ships.