The Platform

Feature parity is the easy half.

You already know what an ERP does. What decides whether an implementation stays profitable is what happens after go-live — when the base upgrades, when the client asks for the fortieth customization, and when someone has to keep it running. Fullfinity makes those three things mechanical instead of manual.

After go-live

Three costs no proposal ever quotes accurately.

None of these is a feature problem. They are all consequences of how the platform underneath handles change.

Version migrations

A quoted project every time, priced against risk nobody can measure until they are inside it. Custom code breaks in ways nothing warned anyone about, and the client remembers the overrun rather than the version number.

Customization drift

View patches that target a position in someone else’s layout. Upstream moves a section, the patch lands somewhere wrong — and it fails silently, in production, weeks later.

Correctness support

Cross-company records that should never have referenced each other. Settings nobody filled in. A duplicate button that quietly creates real documents. Tickets that look like your bugs.

Upgrades

A breaking change fails in CI, not on a client’s server.

Adding a field or a model is free — no script, no version bump. The column is created and existing rows are backfilled from the field’s default or computation automatically. Nobody writes SQL.

Renaming, removing or retyping something that already exists is different. It is a breaking change, and the schema gate refuses to let it out of the repository until it has been declared. You answer one question, and the answer is committed alongside the model change as a numbered, append-only ledger entry.

The ledger is keyed by a monotonic change id, not by a release number — so a database that skipped four releases upgrades in exactly the same single pass as one that did not. Because the clock is a high-water mark, a database cannot be silently downgraded. It refuses.

The sequencefour commands
$ fullfinity-server check --only schema Runs in CI and on pre-commit. Compares the models against the committed baseline and fails the build on any undeclared rename, removal or retype.
$ fullfinity-server resolve Asks what the change actually is — a rename, a delete, or a complex transform — then records it in the ledger and re-snapshots the baseline. A complex transform is rejected without an accompanying fixture test.
$ fullfinity-server preview --db acme_prod Pre-flight against one real install. Lists every change that database is behind on, and the custom references each one will affect, before anything is touched.
$ fullfinity-server -u all The upgrade. Additive migration in a single atomic transaction, then the ledger replays in id order — a rename carries the data across and drops the old column.
Customization

Patch a named anchor, never a position.

Customizations do not drift, because there is nothing positional to drift against. Every targetable section of a view carries a stable, unique anchor, and an inheriting view addresses that anchor.

Rename the section, retitle it, move it to the other column — the contribution still resolves. The targets that look like they work but silently rot are rejected outright, at load time and again in CI.

Rejected by the gate

  • {tab: "Sales"}
  • {index: 2, type: group}
  • {button: action_confirm}
  • {field: incoterm}

Titles get retranslated. Indexes shift. A bare field name means “wherever that field lives now” — so moving it takes the contribution along, resolving cleanly the whole way, into the wrong place.

Required

  • {anchor: sales}
  • {anchor: shipping, field: incoterm}
  • {anchor: totals, field: amount_total}

Scoped to a section that was deliberately named to be extended. If that anchor is ever removed upstream, it goes through the same ledger — so you are told, rather than finding out from a client.

The same holds for no-code customizations. Changes made in the in-app builder are carried across base upgrades by the same mechanism, not re-applied and hoped over. A client who customized their own forms is not a client anyone should be afraid to upgrade.

Guarantees, not conventions

Enforced by the framework, or by a gate that fails the build.

None of these is a guideline someone can forget on a Friday.

Multi-company

One field, both halves

Declaring a company field is the entire opt-in — it turns on read isolation and write validation together. A record can only reference its own company’s records or shared ones; anything else is refused at the write, not discovered in an audit.

Fresh installs

Settings resolve, never block

A configuration value is an override, not a required input. Consuming code resolves a sane default — the mapped account role, the company’s journal of that type — so a brand-new tenant can post an invoice before anyone opens the settings form.

Duplicate

Copying children is opt-in

Duplicating a record copies owned lines and nothing else. It cannot silently spawn real invoices, subscriptions or manufacturing orders, and the relations allowed to be copied are pinned by a test, so widening the list needs a reviewer.

Layout

Settings pages cannot sprawl

Every app extends one settings form under an enforced layout contract — domain panels, then features, then links. A third-party module physically cannot lay its tab out its own way, so the tenth app installed looks like the first.

Isolation

Shared state is namespaced

Modules get a namespaced key-value store for ephemeral state. Reaching past it into the engine’s own cache is blocked at build time and again at server start — including for modules installed on a self-hosted box.

Translation

The English string is the key

No synthetic keys to maintain and no untranslated identifiers leaking into a UI. Field labels, hints, view titles and error messages are extracted from source automatically.

Speed and scale

Concurrency is an architectural property, not a tuning exercise.

The ERPs you are replacing are synchronous. A worker is held for the entire life of a request — including the seconds it spends doing nothing at all, waiting on Postgres, on a payment gateway, on a carrier rate call, on an SMTP handshake. Every one of those idle seconds is a seat nobody else can use, so the only lever left is more workers, more memory, more boxes.

Fullfinity is async Python from the request handler all the way down through the ORM. A request that is waiting holds nothing: the process takes up other work and resumes the moment the result lands. The same hardware carries far more concurrent users, and a third party having a bad afternoon slows the one integration that calls it instead of saturating the pool and stalling everybody's session.

A single database write does not get faster because the stack is async. Everything that happens while it waits does — and that is the difference between an instance that needs a bigger box every time headcount grows and one that does not.

On your numbers

The figure that matters is yours, not ours. Send the shape of the load — concurrent users, document volume, the third parties sitting in your request path — and we will measure it with you on infrastructure you pick, before you commit to anything.

Set-based ORMQuery shape

Bulk creates and updates compile to a single statement rather than a loop of them, and related records are prefetched in one query instead of one per row. The expensive pattern is the one a developer has to opt into.

Processes, not threadsHorizontal

Scale out by worker count on one host or across hosts. Composed models and record caches are shared with database-backed coherence, so no worker serves a stale definition after an upgrade lands.

Off the response pathLatency

Mail, webhooks, document generation and analytics dispatch as background coroutines rather than parking the user’s request behind them. A slow third party degrades one integration, not the whole checkout.

Offline at the edgeAvailability

The point-of-sale register and the field-service client keep operating through a dropped connection and reconcile on reconnect. Distance to the data centre stops being a checkout problem or a site-visit problem.

Under the hood

A modern stack your developers already know.

Modules are declarative: Python model classes, and YAML for views, menus, security and seed data.

Server
Async Python, FastAPI, PostgreSQL
Realtime
Valkey — cache, pub/sub, presence
Client
React, with a Preact island layer for public pages
Module
Python models, YAML views, security and data
Identity
OIDC provider, external SSO, per-seat tokens
Docs
Developer reference plus a generated user help centre
Ownership

What you own, and what happens if we are not here.

The open tier is LGPL and self-hostable. Only the engine, the ORM and the React client are closed — every open module, and everything you or your clients write on top of them, is yours: readable, forkable, and running wherever you choose to put it.

Licensed modules are gated by a signed, per-database token, enforced at seat count on login. When a licence lapses there is a grace period and a self-service downgrade that uninstalls the licensed modules and leaves every open module and all the data intact. Gating hides licensed field values from output; it never drops a column. A client is never walled out of their own database.

Open tier
LGPL, self-hostable, forkable
Licensed
Ed25519-signed per-database token
Seats
Enforced at login, not on the honour system
Lapse
Grace period, then downgrade — no data loss

See it on a real install.

The upgrade sequence, an anchored customization surviving a restructured base view, and the suite itself — walked through with your team.