Skip to content

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.