Concepts
The platform is metadata-driven: the structures that describe data (entities, fields) and the structures that describe the UI (views, forms) are themselves rows in the database. A generic engine reads that metadata at request time and produces the API response, the grid, or the form.
Entity
A metadata-defined data table (e.g. syntec_field). An entity has a kind (transactional or
masterdata), a set of fields, and base-contract columns (id, guid, created, modified,
status, plus cid on masterdata). The generic EntityRepository (app/src/Engine) performs
CRUD and queries against any entity from its metadata — there is no per-entity code.
References between entities are exposed by guid, never the raw integer id: a reference field is resolved through a LEFT JOIN so the API and UI always speak guids.
Field
A column of an entity, with a type (varchar, integer, boolean, temporal types, reference,
…), nullability, length, and flags. Fields drive validation, the SELECT/sort/filter machinery, and
field-level permissions.
View
A configurable grid over an entity, resolved by ViewResolver (app/src/View). A view declares
columns, default sort, page sizes, filters, and per-user variants (saved column
layouts/filters). Filtering is compiled to parameterised SQL by FilterCompiler (only
whitelisted, typed fields are filterable — no injection surface). A view can also be an
escape-hatch raw-SQL view for cross-entity reporting.
Views support parent scoping so a grid can be embedded as a child of a parent record (the
embedded child-grid in forms uses this). An entity view scopes via parent_field — the
engine resolves the parent guid to its row id and adds a parent_field = id filter. A raw-SQL
view scopes via a bound placeholder instead: write :parent_guid in the SQL and the engine binds
it to the parent form's guid (the Syntec Core :guid equivalent), e.g.
SELECT a.id, a.name
FROM item_table a
JOIN parent_table b ON a.parent_table__id = b.id
WHERE b.guid = :parent_guid
Alongside :lang_id, :active, and :user_id, :parent_guid is a system placeholder the raw-view
engine binds only when it appears in the SQL; it is empty (matching no rows) for a top-level or
unsaved parent. See Raw-SQL views for the full placeholder vocabulary,
declaring columns, and wiring a raw-SQL view as a form subview.
Form
A configurable editor for an entity record, resolved by FormResolver (app/src/Form). A form is
a tree of components (tabs, sections, columns, fields, sub-forms, embedded grids). Field
components map to widgets (text, select, boolean, timestamp, relation pickers, …) and carry
validation, conditional visibility, and permissions. Writes go through the same engine; the form
sends a dirty-diff which the engine validates and persists.
Security
Three layers, resolved in app/src/Security:
- Role-based access — principals hold roles; routes and actions check them.
- Field-level permissions — read/write grants per field per role; the engine filters columns it returns and accepts.
- Record (row) level — row policies scope which records a principal may see/change.
Auth itself (app/src/Auth) is EdDSA-signed JWTs with Argon2id password hashing and refresh.
Translations (companion *_lng tables)
A translatable column lives in a companion table named <entity>_lng (e.g. syntec_field_lng),
keyed by the parent id + language. The engine discovers companions by convention
(CompanionLanguageWriter). On read it LEFT JOINs the current language and nests it under the
companion key; writes accept all languages keyed by language code:
{ "syntec_config_lng": { "EN": { "value": "60" }, "NL": { "value": "100" } } }
A single write can also nest a record's _lng companion and inline child-collection arrays —
see Nested writes.
Packages & bootstrap
Functionality ships in packages; the platform owns the Base package. Bootstrap
(app/src/Metadata/Persistence) creates the metadata schema and seeds Base (languages, roles,
seed entities/views/forms). It is idempotent — safe to re-run — and must be re-run after any
DDL or seed change (see Getting started).
OpenAPI
The REST surface is described by OpenApiGenerator (app/src/Api/OpenApi) and served at
/api/docs. Every new route must be added there so the document stays complete; the TypeScript
SDK in sdk/ is generated from it and consumed by the admin app.