Elk gereedschap, op een plek
Deze pagina is de praktische referentie voor elk gereedschap in de sidebar van je Archestack-omgeving. Elke sectie behandelt wat het gereedschap doet, wanneer je ernaar zou grijpen, de stappen voor de meest voorkomende workflows en de valkuilen waar nieuwe gebruikers in stappen. De laatste sectie (Via-script-aanroepbare API's) documenteert het exacte oppervlak dat scripts kunnen aanroepen.
Schema Designer
Een visuele editor voor het database-schema, tabellen, kolommen, types, relaties, indexes, die naar een JSON-document auto-saved. Het schema is de bron van waarheid voor je applicatie; geen ander gereedschap ziet een kolom totdat je hem hier toevoegt en deployt.
Veelvoorkomende workflow: klik in de werkbalk op Add Table
(icoonknop). Er opent een dialoog getiteld "Create New Table"; typ een naam (de hulptekst
herinnert je aan "Lowercase with underscores recommended"), klik op Create. De tabel
verschijnt op het canvas met een automatisch gegenereerde id SERIAL primary
key. Selecteer de tabel, het rechterpaneel opent op de Columns-tab. Voeg kolommen
toe vanuit het "Add column"-vak met gestippelde rand onderaan (naamveld, Type-dropdown, klik
Add Column). Klik op elke kolom om uit te klappen en pas de toggles
(PK, NULL, UQ), Length, en Default value aan.
Kolomtypes beschikbaar in het Type-dropdown: SERIAL, BIGSERIAL, INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, REAL, DOUBLE PRECISION, VARCHAR, CHAR, TEXT, BOOLEAN, DATE, TIME, TIMESTAMP, TIMESTAMPTZ, UUID, JSON, JSONB, BYTEA, INET, CIDR, MACADDR, MONEY, INTERVAL, plus geometrische en array-types. Length is alleen relevant voor VARCHAR en CHAR.
Foreign keys leven in de Relations-tab van het rechterpaneel (niet "Relationships"). Scroll naar "Add relationship": kies de FK-kolom op de huidige tabel, het relatietype (One-to-One / One-to-Many / Many-to-Many), de references-tabel, de references-kolom, en klik op Add Relationship. Elke relatie heeft On Delete en On Update-dropdowns (CASCADE / SET NULL / SET DEFAULT / RESTRICT / NO ACTION).
Auto-save: de indicator linksonder springt tussen "Auto saving..." en "Saved". Er is ook een Save-knop in de werkbalk (alleen geactiveerd wanneer er openstaande wijzigingen zijn) voor expliciet opslaan.
Andere werkbalk-knoppen: Add Group (visuele containers voor gerelateerde tabellen, eigenschappenpaneel getiteld "Group"), Add Text (vrije annotaties, eigenschappenpaneel getiteld "Text Annotation"), zoom-controls, package-filter, tabellenzoekveld.
Valkuilen: een kolom hernoemen produceert een destructief plan (drop + add),
bewerk de SQL op de Generated SQL-tab van de deployment om RENAME COLUMN te
gebruiken als je data wilt behouden. Een NOT NULL-kolom toevoegen aan een niet-lege tabel
zonder default faalt, maak hem nullable, vul hem, en wijzig dan. De Deploy-werkbalk-
knop brengt je naar een deployment-configpagina, niet direct in een deploy.
Database Deployments
Toont elke deployment met zijn status (Draft / Executing / Succeeded / Failed) en draait
nieuwe. Route: /database-deployments/overview.
Knoppen rechtsboven: Refresh (lijst herladen), New Deployment (maak een nieuwe draft aan).
Vanuit Schema Designer: de groene Deploy-rakettknop in de
werkbalk van Schema Designer brengt je naar /database-deployments/configure/new,
een verse deployment-configuratiepagina.
Deployment-configuratiepagina: twee tabs.
- Configuration - pre/post-deployment-scripteditors (PostgreSQL) met elk een Test Run-knop. Pre-script draait voor schema-wijzigingen; post-script daarna. Handig voor backfills, index-herbouw of staging-area-opschoning. Plus een Force Deploy (Allow Data Loss)-toggle voor destructieve operaties.
- Generated SQL - toont de migratie die het platform heeft geproduceerd. De SQL is bewerkbaar. Een Regenerate-knop (refresh-icoon) draait de diff opnieuw als je schema-wijzigingen hebt gemaakt sinds het openen van deze pagina.
Bovenknoppen: Back, Save (slaat de configuratie op als Draft), Deploy (draait de deployment direct). Deploy klik je als laatste.
Valkuilen: deployments worden niet automatisch in een transactie verpakt, een
fout halverwege laat de database in een gedeeltelijke staat achter. Voor riskante migraties,
verpak je statements in BEGIN; ... COMMIT; in het pre-script. De
Force Deploy-toggle slaat veiligheidschecks over; gebruik hem bewust.
Business Entities
Gecureerde views gebouwd op een master-tabel plus joins uit gerelateerde tabellen. Pages binden aan Business Entities, nooit aan ruwe tabellen. Zie Concepten, Business Entities voor het waarom.
Veelvoorkomende workflow: klik op Create. De full-page editor opent met tabs Visual, JSON, Events. Stel Entity Name, Master Table (Autocomplete), en Label Column in (de kolom waarvan de waarde een record vertegenwoordigt in pickers). Sla op, de BE genereert automatisch een lijst van native kolommen uit de master-tabel.
Joins toevoegen: scroll naar de Join Configurator-kaart en klik op Add Join. Elke join opent als een Accordion met: Join Type (INNER / LEFT / RIGHT), From Table, To Table (Autocomplete gegroepeerd op Database Tables / Third Party), From Column, To Column, optionele Type Cast-dropdowns, en een kolompicker voor welke doelkolommen aan de oppervlakte komen.
Aggregaties: schakel op een join Aggregate Mode aan. Voor elke gekozen kolom kies je dan een Aggregate Function: COUNT / SUM / AVG / MIN / MAX / COUNT DISTINCT. "Aantal openstaande contacten op dit Company" is het canonieke voorbeeld.
Run Preview: de editor heeft een Run Preview-knop (met een play-icoon) die de volledige join-configuratie uitvoert en echte rijen in een datagrid toont. Als een joinkolom leeg terugkomt, was de kolom niet aangevinkt in de kolompicker van de join (of is de relatie fout geconfigureerd).
Meerdere BEs per master-tabel: niets weerhoudt je ervan om
customer (sales view) en customer (support view) op dezelfde
master te hebben. Verschillende pagina's, verschillende doelgroep-passende kolommen,
dezelfde onderliggende rij.
Valkuilen: gejoinde en geaggregeerde kolommen zijn read-only. Om het gejoinde label te bewerken, navigeer naar het bronrecord. Geaggregeerde kolommen worden bij elke query herberekend, prima voor honderden rijen, traag op miljoenen.
Page Editor
Configureert de runtime-UI door een Page aan een Business Entity te binden. Het platform genereert secties automatisch op basis van de BE-kolommen; vanaf daar tune je de indeling. Er is geen aparte "template-picker", elke pagina start vanuit dezelfde gegenereerde basislijn en je past hem aan.
Veelvoorkomende workflow: klik op Create. Vul Page Name,
Page Route (bijv. /companies), kies de Business Entity
(Autocomplete). De pagina opent in de editor met tabs Visual, Overview,
Create, Entities, Events, JSON. Pas de automatisch
gegenereerde indeling aan, en zet dan de Published-schakelaar in de bovenste header
op AAN.
Veld-widgettypes (de Type Select op elk veld): Text,
Textarea, Number, Date, Select, Checkbox, Email. Gebruik Select voor
foreign-key-velden en stel de Entity-autocomplete van het veld in op de
gerefereerde BE zodat gebruikers op label kiezen.
Tabs in het detailformulier: klik op Add Tab in de Visual-tab.
Binnen een tab kun je een RelatedGrid-sectie toevoegen die gebonden is aan een
andere BE gefilterd op de PK van het huidige record. Klassiek voorbeeld: een Company-pagina
met "Contacts"- en "Deals"-tabs, elk gefilterd op
company_id = current company's id.
Action-knoppen op de Header-inspector van het detailformulier. Klik op Add in de Actions-sectie. Elke action heeft Label, Icon (Save / Delete / Add / Refresh / Download / Bolt / None), Variant (Contained / Outlined / Text), Color (Primary / Secondary / Error / Success / Warning / Info), en Steps (een geordende lijst van: Save Record, Delete Record, Navigate, Business Events, Custom). Om een Business Event vanuit een knop te draaien, zet je het step-type op Business Events en kies je het event op naam.
Publiceren: de Published-schakelaar in de bovenste header is de enige publicatiebediening. AAN = de pagina verschijnt onder Published Pages in de sidebar en eindgebruikers die naar de route navigeren zien hem. UIT = concept (alleen jij in de editor ziet je wijzigingen).
Valkuilen: de automatisch gegenereerde kolommen op een verse Page spiegelen de BE, als de BE verandert (je voegt een join toe), pikt de Page geen nieuwe kolommen automatisch op. Herlaad de pagina in de editor of voeg de kolom handmatig aan de sectie toe. Er is momenteel geen "Generate PDF"-action-step-type, om een PDF aan een paginaknop te koppelen, expose de PDF via een Script Module die het REST-endpoint aanroept en gebruik een Custom-step (of roep het endpoint direct aan vanuit een Frontend Template).
Business Events
Regels die vuren op data-wijzigingen (of op een schema, of handmatig) en acties draaien wanneer condities zijn voldaan. Het "wanneer X gebeurt, doe Y"-mechanisme van het platform. Zie Concepten, Business Events voor het model.
Veelvoorkomende workflow: klik op Create. Stel Rule Name in, schakel Enabled aan. Kies Business Entity. Vink een of meer Triggers aan: BeforeCreate / BeforeUpdate / AfterCreate / AfterUpdate / BeforeDelete / OnSchedule / Manual / InitialValue. Bouw de conditieboom (AND/OR-groepen). Voeg een of meer acties toe in de FlowCanvas. Sla op.
Actietypes (RuleActionKind):
- Execute Script - draai een C#-script. De meest flexibele actie; grijp ernaar wanneer je een veld wilt zetten, een berekening wilt doen, een API wilt aanroepen, iets custom. Het script krijgt
Entity(muteer op Before*-triggers om de in-flight write te wijzigen),OldEntity,Log,Db,Modules. - Validate - een script dat
boolteruggeeft.truebetekent dat validatie faalt en het opslaan wordt geblokkeerd met het geconfigureerde foutbericht. Markeert optioneel specifieke kolommen via errorColumns. - Block Operation - stopt de operatie hard met een geconfigureerd bericht. Geen script.
- Create Entity - voegt een record in een andere BE in. Veldwaarden ondersteunen template-expressies zoals
{{ Entity.column_name }}en{{ now() }}. - Update Entity - update records in een andere BE die aan een conditiefilter voldoen. Dezelfde template-expressies.
- Delete Entity - verwijdert records die aan een conditie voldoen. Weigert te vuren zonder een conditie (veiligheid).
- Send Email / Send Webhook / Publish Event - in het schema gedefinieerd als toekomstige uitbreidingen; momenteel gelogd en overgeslagen.
"Een veld zetten"-patroon: er is geen aparte Set Field-action. De
manier om een veld te zetten is een Execute Script op een Before*-trigger die
Entity muteert. Voorbeeld: Entity.title = ((string)Entity.title)?.Trim();. Het
platform persisteert de aangepaste Entity als onderdeel van de in-flight write, geen extra
query, geen recursie-risico.
Run Simulation: de Simulate-tab van de trigger-editor heeft een
record-picker en een Run Simulation-knop. Het platform draait je condities en
acties tegen het gekozen record zonder wijzigingen te persisteren: schrijfoperaties worden
gecontroleerd en vastgelegd, maar nooit uitgevoerd, die van een script via Db en
Files inbegrepen, dus er komt niets in de database of de bestanden van de workspace
en er vuurt ook geen ander Business Event. Mail en meldingen worden niet verstuurd. Al het andere
dat een script zelf doet, is echt: HTTP-aanroepen, System.IO, en alles wat een
NuGet-package doet, zoals een verbinding met een andere database.
Het output-paneel toont welke condities matchten en
wat elke actie zou hebben gedaan. Gebruik dit om verkeerde configuraties op te vangen voordat
live data ze raakt.
Template-expressies in actie-configuratie gebruiken
{{ ... }}-haakjes. De daadwerkelijke tokens:
{{ Entity.column }}, {{ OldEntity.column }},
{{ now() }} / {{ getdate() }}, {{ today() }},
{{ guid() }} / {{ newid() }}, {{ year() }},
{{ month() }}, {{ day() }}, {{ timestamp() }},
aggregates als {{ SUM(column) }} binnen join-contexten, en post-substitutie-
rekenkunde als {{ Entity.quantity * Entity.price }}. Er is
geen {{ user.email }}-token.
Conditie-operatoren: Equals, NotEquals, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, Like (= ILIKE), Contains (= ILIKE %value%), IsNull, IsNotNull, In (= ANY(...)).
Valkuilen: een trigger die hetzelfde record muteert dat hij bekijkt zal
zichzelf hervuren als je Update Entity gebruikt; gebruik in plaats daarvan
Before Update + Execute Script + Entity.field = .... Twee
triggers op hetzelfde event vuren op prioriteitsvolgorde. Uitgeschakelde events
verschijnen toch in de lijst, de Enabled-toggle staat los van de trigger-config.
Script Modules
Herbruikbare C#-scripts die runtime door Roslyn worden gecompileerd. Nemen parameters,
bevragen de database via de Db-helper, geven een waarde terug. Aanroepbaar
vanuit Business Events, Scheduled Events, en direct vanuit de front-end.
Veelvoorkomende workflow: klik op Create. De editor opent met tabs
Edit, Parameters, Test, JSON. Geef de module een naam
(PascalCase, bijv. RecalcOpenRevenue). Klik in de Parameters-tab op
Add per parameter (Name, Type, Required, Default Value). Type-opties: string, int,
decimal, double, bool, DateTime. Terug naar de Edit-tab, schrijf C#. IntelliSense
staat aan.
Test-paneel: de Test-tab toont je parameter-invoer links en een Run-knop (PlayArrow-icoon). Klik op Run; de rechterkant toont een succes- of foutindicator, output-log, en de return-waarde (JSON-geformatteerd).
De Db-helper, zie de volledige referentie in Via-script-aanroepbare API's hieronder.
Aanroepen vanuit een Business Event: voeg een Execute Script-action toe waarvan de body de module aanroept:
// The module is a method; its name and parameters are checked when the script compiles
await Modules.RecalcCompanyOpenDealValue(company_id: (int)Entity.company_id);
// Calling it by name still works
await Modules.CallAsync("RecalcCompanyOpenDealValue", new Dictionary<string, object?> {
["company_id"] = Entity.company_id
}); Aanroepen vanuit een Scheduled Event: maak een event met de
On Schedule-trigger, zet de cron, voeg een Execute Script-action toe met
dezelfde Modules.CallAsync-body.
Logging: elke invocation schrijft een entry naar Event Logs met uitvoeringstijd, parameters, en resultaat (of fout).
Valkuilen: altijd await op Db-calls, vergeten compileert maar
geeft een Task terug. Returns worden als object doorgegeven (niet JSON-geserialiseerd door
het framework); voor front-end-consumptie, geef anonieme objecten met primitieve velden
terug. Suggestion-on-commit-character is bewust uitgeschakeld in de editor; gebruik Tab om
te accepteren.
Shared Code
Code die in meer dan één script of pagina nodig is, bewaard in eigen bestanden. De naam van een bestand
is zijn pad, zoals Sales/OrderSummary.cs of components/StatusChip.tsx,
en de extensie zegt wat het is: .cs is C#-code die elk script kan gebruiken, terwijl
.tsx, .ts, .jsx en .js modules zijn die
pagina's, frontend templates en de Code-blokken van app-layouts importeren. Gedeelde bestanden
horen bij packages, zoals elk ander artefact.
Veelvoorkomende workflow: open in de sidebar, onder Logic, Shared Code en klik op New file. Typ het pad; de editor begint het bestand met een voorbeeld van dat soort bestand. Schrijf de code en klik op Save.
C#-bestanden bevatten gewone C#: namespaces, records, classes en statische
helpers. Elk script (script modules, business events, scheduled events, PDF-datascripts en tests)
kan hun publieke types gebruiken. Een type zonder namespace is beschikbaar onder zijn naam; een
type in een namespace heeft een using in het script nodig, of een
global using in een willekeurig gedeeld bestand, die dan voor elk script geldt. Een
gedeeld bestand ziet het getypte datamodel (Order,
WorkspaceDb), maar niet de globals van een script, dus een helper die
Db of Log nodig heeft, krijgt die als argument mee.
// Shared code: Sales/OrderSummary.cs, ordinary C# every script can use
namespace Sales;
/// <summary>One order as the invoices and the dashboard report it.</summary>
public record OrderSummary(int Id, string Number, decimal Total);
public static class Vat
{
public static decimal Add(decimal amount, decimal rate = 0.21m) =>
Math.Round(amount * (1 + rate), 2);
} // A script module using it
using Sales;
var open = await Db.Order.Where(o => o.Status == "Open").ToListAsync();
return open.Select(o => new OrderSummary(o.Id, o.Number, Vat.Add(o.Total ?? 0))).ToList(); Een opgeslagen wijziging bereikt elk script bij zijn volgende run. De editor controleert een gedeeld bestand samen met de andere gedeelde bestanden, en Go to Definition op een gedeeld type in een script opent het bestand dat het declareert. Als de gedeelde code niet compileert, falen de scripts die ervan gebruikmaken, met de naam van het bestand dat fout is; scripts die er geen gebruik van maken, blijven draaien. Een tabelwijziging die een gedeeld bestand zou breken, wordt gemeld samen met de scripts die erdoor breken, voordat de deployment draait.
Modules worden geïmporteerd uit @/, gevolgd door hun pad zonder de
extensie: components/StatusChip.tsx is
import { StatusChip } from "@/components/StatusChip". Een pagina kan
naast de views ook eigen bestanden hebben, toegevoegd met + boven de code-editor en
geïmporteerd als ./OrderRow. Een module ziet dezelfde globals als paginacode en
exporteert wat andere bestanden gebruiken. Een view die iets importeert, mag eindigen met een
default export van zijn component in plaats van met return. Code zonder
import of export draait precies zoals altijd.
// Shared code: components/StatusChip.tsx, a module pages import
export function StatusChip({ status }: { status: string }) {
const color = status === "Open" ? "warning" : status === "Paid" ? "success" : "default";
return <Chip size="small" color={color} label={status} />;
} // A page's overview, with a file of its own, OrderRow.tsx, beside it
import { StatusChip } from "@/components/StatusChip";
import { OrderRow } from "./OrderRow";
export default function Orders() {
const [rows, setRows] = useState([]);
useEffect(() => {
backend.getBusinessEntityData("Order").then((r) => setRows(r.rows.items));
}, []);
return (
<Stack spacing={1}>
{rows.map((row) => (
<OrderRow key={row.id} row={row}>
<StatusChip status={row.orders_status} />
</OrderRow>
))}
</Stack>
);
}
De packages die code kan importeren, zijn de packages die de app al meelevert, en een import is
hetzelfde object dat ook in de globals van de pagina zit: react,
@mui/material, @mui/x-date-pickers, dayjs,
lucide-react, three, @react-three/fiber en
@react-three/drei. Andere packages zijn er niet.
Publiceren: de gepubliceerde app leest de gedeelde modules zoals de huidige release ze heeft vastgelegd, dus een bewerking bereikt eindgebruikers bij de volgende publicatie, net als bij een pagina. Een bezoeker die niet is aangemeld, krijgt alleen de modules die openbare pagina's en openbare app-layouts importeren.
Valkuilen: het pad is de identiteit van een bestand in elke werkruimte, en
imports verwijzen ernaar, dus een bestand hernoemen breekt de bestanden die het importeren, tot
hun imports de nieuwe naam volgen. Paden gebruiken letters, cijfers, punten, underscores en
koppeltekens. De namespace Archestack is gereserveerd. Een alias in een
global using (global using Money = decimal;) wordt niet doorgegeven
aan scripts.
NuGet Packages
Bibliotheken van NuGet voor de C#-scripts van de workspace: een driver voor een andere database,
een CSV- of Excel-lezer, een client voor een dienst. Elk script kan ze gebruiken (script modules,
scripts van business events, scheduled events, PDF-datascripts, testscripts en codechecks), en de
gedeelde .cs-bestanden ook. De scripteditor en VS Code vullen hun types aan. Een
workspace die er geen toevoegt, draait zijn scripts precies zoals voorheen.
Veelvoorkomende workflow: open in de sidebar, onder Logic, NuGet Packages en klik op Add NuGet package. Zoek ernaar, of typ zijn exacte id als de bron niet doorzocht kan worden, en kies een exacte versie: de nieuwste stabiele versie is voorgeselecteerd, en Include prerelease versions toont de andere. Kies bij Belongs to waar hij bij hoort: een van de packages van de workspace, of Workspace only. Add downloadt hem eerst met zijn afhankelijkheden op de server. Lukt dat niet, dan zegt het dialoogvenster waarom en wordt er niets opgeslagen; lukt het wel, dan toont het de namespaces die hij meebrengt.
In een package: een NuGet-package die bij een package hoort, reist met die package
mee, in zijn export en in wat een pipeline deployt, en wie een package installeert, ziet eerst welke
NuGet-packages erbij komen of een hogere versie krijgen, voordat er iets verandert. In een repository
staan ze in packages/<package>/nuget.json, dat alleen geschreven wordt als de
package er declareert, en VS Code controleert het bestand terwijl je typt. Een push die het bestand
wijzigt, downloadt ze eerst, en wordt geweigerd, zonder iets te veranderen, als ze niet binnengehaald
kunnen worden.
{
"packages": [
{ "id": "CsvHelper", "version": "33.0.1" },
{ "id": "Dapper", "version": "2.1.35" },
{ "id": "MySqlConnector", "version": "2.4.0" }
]
} Er een gebruiken: een NuGet-package brengt types mee, nooit een global, en er wordt
niets voor je geïmporteerd. Schrijf een using voor zijn namespace, die vaak niet de id
is: de kolom Namespaces op de pagina toont de namespaces van elke NuGet-package als
using-regels om te kopiëren. Geef vanuit een script gewone waarden terug, niet de eigen
objecten van de bibliotheek.
// NuGet package: CsvHelper. Reads a price list an admin uploaded on the Files page.
using System.Globalization;
using System.IO;
using CsvHelper;
var text = await Files.ReadTextAsync("imports/prices.csv");
var csv = new CsvReader(new StringReader(text), CultureInfo.InvariantCulture);
var rows = new List<object>();
try
{
csv.Read();
csv.ReadHeader();
while (csv.Read())
rows.Add(new { Sku = csv.GetField("sku"), Price = csv.GetField<decimal>("price") });
}
finally
{
csv.Dispose();
}
// Plain rows, not CsvHelper objects.
return rows; // NuGet packages: MySqlConnector and Dapper. A script module that lists the open invoices in
// the old ERP's MySQL database. The connection string is a file an admin uploads on the Files
// page of each environment: a password written in a script travels to git, package exports
// and every environment.
using Dapper;
using MySqlConnector;
var connectionString = await Files.ReadTextAsync("config/erp-connection.txt");
var connection = new MySqlConnection(connectionString.Trim());
try
{
await connection.OpenAsync();
var invoices = await connection.QueryAsync(
"SELECT number, customer_number, amount FROM invoices WHERE paid = 0 ORDER BY number");
// Plain values, not driver objects.
return invoices
.Select(i => new { Number = (string)i.number, Customer = (string)i.customer_number, Amount = (decimal)i.amount })
.ToList();
}
finally
{
// A top-level using var does not parse in a script, so the connection is closed here.
await connection.DisposeAsync();
} Versies: een declaratie noemt één exacte versie, zoals 2.1.35, nooit
een bereik. Als meerdere packages om verschillende versies van dezelfde NuGet-package vragen, gebruikt
elk script in de workspace de hoogste. De kolom In use toont welke versie dat is en wie
erom vroeg.
Wiens kopie draait: een workspace laadt zijn eigen kopie van elke NuGet-package,
gescheiden van elke andere workspace op de server, ook als de server dezelfde bibliotheek zelf
gebruikt. .NET zelf en de bibliotheken die scripts al hebben, zoals System.Text.Json,
logging, EF Core en Npgsql, blijven de kopie van de server. Een NuGet-package die een nieuwere versie
van een daarvan vraagt, krijgt een waarschuwing: hij draait op de kopie van de server, en een methode
die alleen de nieuwere versie heeft, faalt wanneer ze wordt aangeroepen.
Restores: het eerste script dat de NuGet-packages op een server nodig heeft, laat die server ze downloaden. Het wacht tot vijf seconden, en krijgt het verzoek om zo meteen opnieuw te draaien als het downloaden langer duurt. Na een wijziging haalt de workspace de nieuwe set binnen. Tot dat lukt, houden scripts de laatste set die wel binnengehaald werd, dus een mislukte restore neemt nooit NuGet-packages weg die werkten. De banner op de pagina toont de stand van zaken en de fouten van een mislukte restore met uitleg. Na een mislukking probeert de server het opnieuw de eerste keer dat een script de NuGet-packages nodig heeft na het tijdstip in de banner, of meteen met Restore now. Resolved somt elke NuGet-package op die scripts gebruiken, afhankelijkheden inbegrepen.
Proefruns en tests: een proefrun (de Test-tab, Run Simulation)
houdt alleen tegen wat via Archestack gaat: schrijfacties via Db en Files
worden gesimuleerd en niet opgeslagen, en mail en meldingen worden niet verstuurd. Een testrun doet
zijn schrijfacties via Db in zijn sandbox en houdt de rest op dezelfde manier tegen.
Al het andere dat het script zelf doet, is echt: HTTP-aanroepen, System.IO, en alles
wat een NuGet-package doet, zoals een verbinding met een andere database, dus een schrijfactie via
een driver gebeurt bij elke run. Laat zo'n script naar een testdatabase wijzen terwijl je het
uitprobeert.
Valkuilen:
- NuGet-packages hebben getypte datatoegang nodig, net als gedeelde C#. Op een server die die uitzet (
Scripting:TypedData:Enabled), zegt de pagina dat en kan er geen worden toegevoegd. #r "nuget: Dapper, 2.1.35", zoals andere scriptrunners er een laden, werkt hier niet. Voeg hem toe op de pagina.- Een native bibliotheek die een NuGet-package meebrengt, wordt één keer per serverproces geladen, wordt gedeeld door de workspaces die hetzelfde bestand gebruiken, en blijft geladen tot de server herstart.
- NuGet-packages met Workspace only worden niet met een package geëxporteerd, staan niet in de repository en worden niet door pipelines gedeployd. Geef een NuGet-package die een app nodig heeft aan de package van die app.
- Een top-level
using varwordt in een script niet geparsed, dus sluit een verbinding zelf, in eenfinally, zoals het voorbeeld doet. - Een waarde uit
Entityisdynamic, en een extension method zoalsQueryAsyncvan Dapper kan er geen als argument aannemen: het script compileert niet (CS1973). Cast hem eerst:(string)Entity.customers_number. - NuGet-packages die tools, templates of MSBuild SDK's zijn, worden geweigerd, en ook een die niets bevat waartegen scripts op .NET 8 kunnen compileren, of een die de eigen PostgreSQL-driver of EF Core van de server uitbreidt. Ids met de naam
OmniCoreofArchestack, of die beginnen met een van beide en een punt, zijn van het platform.
Voor serverbeheerders: NuGet-packages staan standaard aan en in een trial altijd
uit. De instellingen staan onder Scripting:NuGet, of als omgevingsvariabelen
Scripting__NuGet__Enabled enzovoort. Elke server downloadt voor zichzelf, dus elke
server moet nuget.org of de hier geconfigureerde feeds kunnen bereiken, via de proxy in
https_proxy, http_proxy en no_proxy als die er is. Geef elke
server van één installatie dezelfde bronnen.
| Instelling | Standaard | Wat het doet |
|---|---|---|
Enabled | true | Uit: er wordt niets gedownload en scripts laden er geen. De pagina kan er geen toevoegen; declaraties die een import of push meebrengt, blijven ongebruikt bewaard. |
Sources | nuget.org | Waar NuGet-packages vandaan komen, gescheiden door ;: feeds via hun v3-service-index (de URL die eindigt op index.json), of absolute mappen, waaruit gekopieerd wordt en die vertrouwd worden: de downloadlimieten gelden alleen voor feeds. nuget.org wordt alleen gebruikt als noch deze instelling noch Feeds een bron noemt. |
Feeds:<name>:Url, Username, Password | Geen | Een feed met een naam, voor een feed die toegangsgegevens nodig heeft. De URL bevat nooit een gebruikersnaam of wachtwoord. |
Feeds:<name>:Patterns | Geen | De ids die deze feed levert, gescheiden door ; (Contoso.*). Zodra een feed patronen heeft, komt een id van de feed waarvan het patroon erop past, en levert een feed zonder patronen al de rest. |
Feeds:<name>:AllowInsecureConnections | false | Laat een gewone http://-feed door. |
Directory | Een map in de lokale applicatiegegevens | Waar deze server de NuGet-packages bewaart die hij heeft gedownload: één map per server op zijn lokale schijf, nooit een volume dat servers delen. Geef hem in een container een eigen volume van die server, anders downloadt elke nieuwe container ze opnieuw. |
MaxPackageBytes, MaxExtractedBytes | 100 MB, 1 GB | De grootste NuGet-package die een restore downloadt, en hoeveel één NuGet-package uitgepakt hoogstens mag innemen. |
MaxRestoreBytes, MaxRestoreExtractedBytes, MaxPackages | 500 MB, 2 GB, 150 | Hoeveel één restore hoogstens downloadt, uitpakt en aan NuGet-packages telt, alles samen. |
MaxCacheBytes | 10 GB | Daarboven neemt de map geen nieuwe restores meer aan, en worden de NuGet-packages die deze server niet gebruikt verwijderd, de langst ongebruikte eerst. |
MinFreeDiskBytes | 1 GB | Met minder vrije schijfruimte dan dit start een restore niet, en wordt een download gestopt die er minder zou overlaten. |
RestoreTimeoutSeconds, AddWaitSeconds, MaterializeWaitMilliseconds | 300, 120, 5000 | Hoe lang een restore mag duren, hoe lang het toevoegen van een NuGet-package op zijn download wacht, en hoe lang een script op de download van deze server wacht voordat het gevraagd wordt opnieuw te draaien. |
AllowNativeAssets | true | Uit: een NuGet-package die native bibliotheken bevat, wordt geweigerd, en er wordt er nooit een geladen. |
AuditRefuseLevel | Leeg | low, moderate, high of critical: een NuGet-package met een bekende kwetsbaarheid op of boven dat niveau wordt geweigerd. Leeg weigert er geen, en kwetsbaarheden verschijnen als waarschuwingen; elke andere waarde laat elke restore mislukken die een NuGet-package toevoegt. |
CertificateRevocationMode | online | online of offline: hoe de ondertekeningscertificaten van NuGet-packages op intrekking worden gecontroleerd. |
Frontend Templates
Herbruikbare UI-fragmenten geschreven in TSX die de page editor in een pagina kan droppen. Handig wanneer geconfigureerde templates niet genoeg zijn, op-maat-gemaakte grafieken, ongebruikelijke indelingen, integraties met third-party-widgets.
Veelvoorkomende workflow: klik op Create, geef de template een
naam, schrijf een TSX-component in de editor. De component krijgt props:
record (de huidige rij wanneer ingebed in een detailformulier),
refresh (data opnieuw ophalen), en een paar helpers. Sla op. Verwijs naar de
template vanuit een Page Editor-veld.
Packageable: templates stromen door het Package-systeem als elk ander config-object, ze worden mee-gecascadeerd wanneer een pagina die ze gebruikt aan een package wordt toegevoegd.
Valkuilen: de TSX kan geen willekeurige npm-packages importeren, alleen
gedeelde code uit @/ en de paar packages die de app
meelevert.
PDF Templates
HTML + Scriban-template + een klein C#-datascript dat naar een PDF rendert (headless Chromium). Elke template is op naam aanroepbaar vanuit een script via REST of direct vanuit een knop op een Page (met een kleine adapter, zie hieronder).
Veelvoorkomende workflow: klik op + Create. De editor opent met Name- en Description-tekstvelden bovenaan, vijf tabs aan de linkerkant (HTML Template, Data Script, Settings, Params, JSON), en een altijd-aan-staand PDF Preview-paneel rechts. De Data Script- en HTML Template-tabs openen elk een Monaco-editor in de linkerhelft; de preview rendert opnieuw wanneer je opslaat (groene Update-knop rechtsboven).
Declareer parameters in de Params-tab via de + Add-knop (Name, Type, Required, Default Value). Het tab-label wordt bijgewerkt naar Params (n) met de telling. Paginaformaat / orientatie / marges / schaal / header / footer leven in de Settings-tab.
Datascript-vorm (zie PDF Invoices tutorial voor een echte):
var deal = await Db.GetAsync("deal", deal_id);
var lines = await Db.From("deal_line")
.Where("deal_id", "=", deal_id).ToListAsync();
return new {
InvoiceNumber = $"INV-{deal.id:D6}",
Lines = lines.Select(l => new { l.description, l.quantity })
}; HTML-template-vorm (Scriban, vergelijkbaar met Liquid/Handlebars):
<h1>{{ InvoiceNumber }}</h1>
<table>
{{ for line in Lines }}
<tr><td>{{ line.description }}</td><td>{{ line.quantity }}</td></tr>
{{ end }}
</table> HTML-encoding: elke {{ }}-waarde wordt
HTML-gecodeerd, dus een recordwaarde met <b> verschijnt als de tekst
<b>. html.raw is de expliciete opt-out, alleen voor
vertrouwde HTML: {{ notes | html.raw }}. Gebruik het nooit op
waarden die eindgebruikers kunnen typen, want dan komt hun markup in het gerenderde document
terecht. Alleen het template kan de encoding uitschakelen, de data niet.
html.escape en html.newline_to_br blijven werken
(newline_to_br escapet de tekst maar behoudt de <br />-tags).
Preview-paneel: altijd zichtbaar aan de rechterkant van de editor. Rendert opnieuw wanneer je opslaat (groene Update-knop). Bevat een thumbnail-kolom voor PDFs met meerdere pagina's en een ingebouwde viewer-werkbalk (zoom, draaien, downloaden, printen). De icoonknoppen rechtsboven in de editor schakelen de zichtbaarheid van het preview-paneel en full-screen-editing aan/uit.
Aanroepen vanuit buiten de editor:
POST /api/v1/pdf-templates/{id}/preview- rendert op ID, geeftapplication/pdfterug. Gebruikt door het preview-paneel van de editor.POST /api/v1/pdf-templates/generate/{name}- rendert op naam, geeftapplication/pdfterug.POST /api/v1/pdf-templates/generate/{name}/base64- hetzelfde maar geeft{ "data": "<base64>" }terug.
Geef parameterwaarden mee in de JSON-body. Auth is de standaard Bearer-token.
Trial-beperking: zowel het generate-endpoint als het
{id}/preview-endpoint (gebruikt door het preview-paneel van de
editor) geven 403 terug in trial-modus, PDF-rendering is volledig afgeschermd. Je kunt nog
steeds de template schrijven (data script, HTML, settings, parameters) en opslaan; je ziet
alleen geen gerenderde PDF totdat je op een betaalde omgeving zit.
Note over via-script-aanroepbare rendering: de C#-scriptcontext exposeert wel
een Pdf-global, dus een Script Module rendert een opgeslagen template rechtstreeks
met await Pdf.GenerateAsync("invoice", ...) of Pdf.GenerateBase64Async(...).
Een HttpClient-omweg naar het generate-endpoint is niet nodig. (De
autocomplete van de scripteditor adverteert nog steeds Pdf.RenderAsync en een
Templates-object; geen van beide namen bestaat at runtime.)
Valkuilen: Scriban escapet HTML standaard. Paginabreuken zijn CSS-gestuurd,
page-break-before: always op een sectie. Lettertypes beschikbaar op de
rendering-server zijn beperkt tot systeemlettertypes plus de Noto- en Liberation-families,
embed je eigen via base64 @font-face als je een merklettertype nodig hebt.
Afbeeldingen, lettertypes en stylesheets: de renderer laadt resources alleen
van publieke http(s)-adressen, plus data:- en
blob:-URL's. Interne adressen en adressen in een privénetwerk
(localhost, 10.x, 192.168.x, servicenamen van
containers enzovoort) worden geblokkeerd, dus een afbeelding van een interne service
verschijnt niet in de PDF. Gebruik een publieke URL of een data:-URL.
Scheduled Events
Hetzelfde Event Trigger-systeem als Business Events, met de On Schedule-trigger aangevinkt. Gebruik het voor nachtelijke herberekeningen, weekelijkse digests, periodieke opschoning, maandelijkse rapporten.
Veelvoorkomende workflow: maak een Business Event met de On Schedule-
trigger (in plaats van, of naast, de data-wijzigings-triggers). Configureer de
cron-expressie. Voeg een Execute Script-action toe waarvan de body een Script
Module aanroept via Modules.CallAsync(...).
Cron-expressies: Quartz-stijl 6 of 7 velden, zie Concepten, Scheduled Events voor veelvoorkomende patronen. Tijden zijn servertijd (UTC op de productie-stack).
Valkuilen: een langlopende job die zijn cron-interval overschrijdt stapelt niet, Quartz zal geen tweede instantie van dezelfde job tegelijk vuren, de tweede tick wordt overgeslagen. Mislukte runs proberen niet automatisch opnieuw; bouw retry in het script in.
Packages
Exporteerbare bundels van configuratie-objecten (BEs, pages, events, scripts, templates, schema) met cascading-afhankelijkheden. Een Page aan een package toevoegen trekt automatisch alles mee waar die pagina naar verwijst.
Veelvoorkomende workflow (export): klik op Create, geef de package een naam en versie. Voeg de top-level-objecten toe die je belangrijk vindt, meestal een handjevol pagina's. Open het linked-items-paneel om de gecascadeerde lijst te zien (wat de package daadwerkelijk gaat bevatten). Vink uit wat je wilt weglaten. Beslis of je data wilt meenemen (toggle bij export). Klik op Export, je krijgt een ZIP.
Veelvoorkomende workflow (import): open op de bestemmingsomgeving Packages, klik op Import, upload de ZIP. Het platform toont wat zal worden toegevoegd of bijgewerkt. Schema-deltas worden niet automatisch toegepast, als de package verwijst naar een kolom die niet bestaat op de bestemming, faalt de import.
Valkuilen: identifiers zijn op naam, niet op numeriek ID. Een BE genaamd "customer" op de bron bindt aan een BE genaamd "customer" op de bestemming, hernoem een van beide en de link breekt.
In git: de VS Code-extensie pullt een package naar een map, als één bestand per artefact, en pusht de map terug, zodat een package gereviewd en geversioneerd kan worden zoals elke andere code.
Third-Party Data Connections
Verbindingen met externe databases (Postgres, SQL Server, MySQL, andere) die tabellen volgens een schema importeren. Geimporteerde tabellen gedragen zich als native tabellen, ze verschijnen in Schema Designer en kunnen worden gerefereerd door Business Entities, Pages en Scripts.
Veelvoorkomende workflow: maak een verbinding aan (connection string, test, opslaan). Toon beschikbare remote tabellen; kies welke te importeren. Stel voor elke geimporteerde tabel sync-mode (Full / Delta) en sync-trigger (cron / handmatig / vast interval) in.
Gespiegeld, niet gefederalisseerd: de geimporteerde data leeft in je eigen
omnicore-db. Reads zijn snel (lokale Postgres). Trade-off: staleness tussen
syncs.
App-managed kolommen: markeer sommige kolommen als "managed by Archestack", ze worden niet overschreven door toekomstige syncs. Handig wanneer je vendor-data wilt annoteren met je eigen statusvlaggen.
Valkuilen: de eerste sync van een grote tabel kan even duren. Delta-syncs
vereisen een kolom die de connector als watermark kan gebruiken, meestal een
modified_at-timestamp.
AI Assistant
Een chat-assistent ingebouwd in de app die zowel vragen over je data kan beantwoorden als je omgeving kan wijzigen. Vraag "hoeveel openstaande werkorders per vestiging deze maand?" en hij bevraagt en antwoordt. Vraag "voeg een status-kolom toe aan service-afspraken en een pagina om ze te tonen" en, als je beheerder bent, bouwt hij het.
Breng je eigen model mee. Archestack levert geen LLM en betaalt geen inferentie. Een beheerder voegt een provider toe op het AI Configuration-scherm: een base URL, een modelnaam en een API-sleutel. Alles dat de OpenAI chat-completions API spreekt werkt (ChatGPT, DeepSeek, OpenRouter, Groq, Together, een zelfgehoste Ollama of vLLM), en de eigen API van Anthropic wordt direct ondersteund. Je kunt er meerdere instellen en er per gesprek een kiezen.
Alles wat hij doet gaat via de MCP Server. De assistent heeft geen eigen achterdeur naar het platform, hij roept dezelfde tools aan als elke externe MCP-client, met jouw identiteit. De limieten die je daar instelt gelden dus ook voor hem, en elke actie belandt in dezelfde audit log. Zie MCP Server hieronder.
Veelvoorkomende workflow: open de assistent vanuit de sidebar of de zwevende bubbel. Vraag het in een of twee zinnen. Hij leest eerst je huidige schema, vertelt wat hij gaat wijzigen, en wacht op je bevestiging. Elke tool-aanroep verschijnt onder het antwoord, zodat je ziet wat er echt gebeurd is.
Werkt goed voor: ad-hocvragen over je data, CRUD-vormige features, simpele automatiseringen, uitbreidingen op bestaande entities.
Minder goed voor: niet-vanzelfsprekende businessregels en complexe meerstaps-workflows. Behandel wat hij bouwt als de PR van een junior dev.
Valkuilen: hij werkt op live data en er is geen undo, dus maak eerst een back-up voor iets riskants. Je prompts en wat de tools teruggeven gaan naar de provider die je hebt ingesteld, onder diens voorwaarden; richt hem op een zelfgehost model als dat uitmaakt. Lange gesprekken herhalen alleen de recentste beurten, dus een chat die de hele dag openstaat weet het begin niet meer. En let op de twee schakelaars: met de MCP Server gestopt kan de assistent nog praten, maar niets lezen of wijzigen.
MCP Server
Elke omgeving kan zichzelf blootstellen via het Model Context Protocol, zodat een AI-client buiten Archestack, Claude Desktop, Claude Code, ChatGPT deep research, een IDE of je eigen script, er direct mee kan werken. Het is ook waar de ingebouwde AI Assistant op draait.
Standaard uit, en de tools die wijzigen hoe de applicatie gebouwd is blijven uit, ook nadat je hem gestart hebt. Dat is bewust zo.
Drie groepen tools. Read dekt het opsommen en beschrijven van entities, rijen bevragen, en het lezen van schema, pages, events, script modules, packages, translations en event logs. Write is aanmaken, wijzigen en verwijderen van records, en gaat via hetzelfde pad als de UI, dus je Business Events vuren precies zoals bij iemand die op Opslaan klikt. Admin is de groep waarmee een model de applicatie zelf kan bouwen: entities, schema, pages, events, script modules, translations, packages en de NuGet-packages die scripts gebruiken.
Vier poorten bepalen of een aanroep landt: draait de server, is de groep toegestaan, staat die specifieke tool aan, en heeft het token de scope. Een tool die door een van die poorten geblokkeerd wordt, is onzichtbaar voor de client in plaats van achteraf geweigerd.
Tokens. Geef er een uit per client op het MCP Server-scherm, met de scopes die hij echt nodig heeft en een optionele vervaldatum. Het geheim wordt eenmalig getoond, bij aanmaak, en er wordt alleen een hash opgeslagen, dus een verloren token roteer je in plaats van dat je hem terugvindt. Geef elke integratie zijn eigen token: dat houdt de activity log leesbaar, en het intrekken van de een verstoort de ander niet.
Activity log. Elke aanroep wordt vastgelegd, ook de geweigerde en waarom. Hier controleer je wat een integratie werkelijk heeft gedaan.
Valkuilen: een token met de admin-scope wordt door alles verderop als volwaardige beheerder behandeld, dus wees er zuinig mee. "Allow token in URL" bestaat voor clients die geen Authorization-header kunnen sturen, maar een URL belandt veel makkelijker in logs en screenshots dan een header, laat het uit tenzij je het nodig hebt. En op een productieomgeving met echte data: laat de admin-groep uit.
Object Browser
Ruwe view op elke native en geimporteerde tabel, voeg rijen direct toe, bewerk, verwijder. Bevat een CSV-import voor bulk-laden.
Wanneer ernaar grijpen:
- Bulk-laden van referentiedata voordat er pagina's bestaan (landen, valuta, status-enums).
- Debugging, zien wat er werkelijk in een tabel zit wanneer een pagina zich vreemd gedraagt.
- Snelle admin-bewerkingen op data die nog niet via een Page is geexposed.
- CSV-import: kies een tabel, upload een bestand, koppel kolommen aan velden, preview, commit.
Valkuilen: Object Browser vuurt wel Business Events af (hij gaat door hetzelfde EntityService-schrijfpad als de front-end). De uitzondering is directe SQL via Database Deployments, dat omzeilt alles.
Dependency Graph
Een alleen-lezen kaart van hoe de configuratie-objecten in je omgeving naar elkaar verwijzen. Acht resourcetypes verschijnen als nodes, gekleurd per type: Business Entities, Pages, Event Triggers, Script Modules, PDF Templates, Frontend Templates, Scheduled Events en Packages. Een edge tussen twee nodes betekent dat de ene resource van de andere afhangt. Grijp ernaar om de vragen te beantwoorden "wat breekt er als ik dit verwijder?" en "is hier iets ongebruikt?".
Wat een edge betekent: een Page gekoppeld aan een Business Entity, een Event
Trigger geregistreerd op een Business Entity, een Page-actiestap die een Event Trigger
afvuurt, een script dat een andere Script Module aanroept via
Modules.CallAsync("Name"), een script dat een PDF Template rendert, een Scheduled
Event die een Event Trigger afvuurt, of een Package dat een resource bevat. Het is een
design-time view, het weerspiegelt wat je configuratie aan elkaar knoopt, niet live
runtime-verkeer.
Twee soorten problemen worden visueel uitgelicht:
- Loose ends (orphans) - nodes waar niets naar verwijst en die zelf nergens naar verwijzen. Ze worden verzameld in een rood omrande "Loose ends"-band onderaan het canvas. Een Script Module die niemand aanroept; een Page die nergens aan gekoppeld is.
- Dangling references - getekend als rode stippellijn-edges. De verwijzing bestaat in de configuratie maar het doel is niet te vinden, bijvoorbeeld een Page-actiestap die nog naar een verwijderde Event Trigger wijst, of een script dat een Script Module aanroept met een naam die niet meer bestaat.
Veelvoorkomende workflow: open Dependency Graph vanuit de Tools-sectie van de sidebar. Het linkerpaneel heeft een naamzoekveld, een Layout-keuze (Left to right, Top to bottom, By type, Force-directed), de toggles Group by package, Show loose ends en Show edges, en een checklist per type. Klik op een node om focusmodus te activeren: die node en zijn directe buren blijven helder terwijl de rest van de graph vervaagt, zodat je precies kunt traceren wat een resource raakt. Klik op de canvas-achtergrond om de focus te wissen. Elke node heeft een kleine knop die de resource in een nieuw tabblad opent, recht naar de editor ervan. De statsregel bovenaan toont het aantal nodes, edges, orphans en dangling references.
Group by package: omkadert de leden van elk package in een gelabelde stippellijn-box, zodat je een package als geheel ziet en opmerkt welke resources het via cascade heeft binnengehaald.
Valkuilen: de graph wordt op aanvraag opgebouwd uit de huidige staat,
gebruik de refresh-knop nadat je elders resources hebt gewijzigd. Script-afgeleide edges
worden gevonden door scriptlichamen te scannen op de letterlijke
Modules.CallAsync("Name")-aanroepvorm, een module die via een berekende naam
wordt aangeroepen toont geen edge. Een dangling reference is altijd het oplossen waard; een
orphan vaak niet, een gloednieuwe Script Module geldt als orphan totdat iets hem aanroept.
Event Logs
Het audit-spoor voor alles wat het platform heeft gedraaid, Business Events, Script Modules, Scheduled Events, third-party-syncs.
Veelvoorkomende workflow: open Event Logs, zet Status op Failed, klik op een entry. Het detailpaneel toont de payload (de staat van het record op het moment, parameters meegegeven), de exception met stack trace, en timing.
Logs en Charts: de pagina heeft twee tabbladen die één filterrij delen. Logs is de tabel hierboven. Charts is een operationeel dashboard over hetzelfde filter: volume in de tijd gestapeld op status, een trend van de succesratio, een uitsplitsing per source, de meest falende en de traagste events, de gemiddelde en maximale duur in de tijd, een uitsplitsing per trigger-type, en een heatmap van dag van de week tegen uur van de dag, met de belangrijkste totalen bovenaan. Beide tabbladen delen een Van / Tot datumbereik (met presets Laatste 1u / 24u / 7d); het Charts-tabblad kiest automatisch een passende tijdsbucket voor het bereik en kan met een interval vernieuwen voor live monitoring.
Real-time meldingen: het belicoontje in de bovenste balk van de adminapp laat ongelezen fouten zien via SignalR.
Valkuilen: de log kan groot worden. Oude entries blijven standaard voor altijd bewaard; als schijfruimte een zorg is, zet een Scheduled Event op die entries ouder dan N dagen opruimt.
User Management
Maak, nodig uit en beheer gebruikers; wijs realm-rollen toe. Wraps de Keycloak admin REST API.
Rollen:
- admin - alles; kan alle Business Units, gebruikers, rollen en system settings beheren.
- owner - beheert eigen Business Units, kan administratie- en runtime-pagina's bewerken.
- editor - kan runtime-pagina's bekijken en bewerken, maar geen schema/BE/event-configuratie wijzigen.
- user - alleen-lezen op runtime-pagina's.
Valkuilen: een gebruiker midden in een sessie degraderen vanaf admin schopt hen er niet uit, de wijziging wordt actief bij de volgende token-refresh (meestal binnen een minuut).
Business Units
Door Keycloak ondersteunde groepen die zichtbaarheid op individuele resources afschermen. Zie Concepten, Business Units voor het model.
Veelvoorkomende workflow: maak een BU aan. Nodig leden uit via e-mail en kies de rol. Wijs resources toe door naar elke BE / Page / Trigger te gaan of via het linked-items-paneel van de BU.
Cascading-toewijzingen: wanneer je een Page aan een BU toewijst, cascadet de toewijzing naar de BE waaraan de pagina bindt.
Valkuilen: de actieve BU van een gebruiker wordt gezet in de
BU-switcher in de bovenbalk en blijft in localStorage. Nieuwe gebruikers gaan
standaard naar hun eerste BU; als een lijstpagina verdacht leeg is, controleer welke BU
actief is.
Database Backups
Nachtelijke pg_dump van alle drie de databases, plus een ZIP van de
bestanden van de werkruimte. Handmatige trigger / upload / restore vanuit
de UI.
Veelvoorkomende workflow: Trigger now, Download, Upload, Restore. Schema staat standaard op nachtelijks om 03:00 UTC.
Trial-beperking: Backups zijn uitgeschakeld in de trial-omgeving.
Branding
Wijzig de weergavenaam van de app, het logo (licht + donker), kleuren, lettertypen en de rest van het Material UI-thema, runtime toegepast, geen rebuild.
Veelvoorkomende workflow: stel de app-naam in, upload een licht logo en een donker logo (PNG/JPG/SVG, maximaal 10 MB), kies de merkkleuren op het tabblad Theme. Sla op: je eigen sessie toont het meteen, die van anderen bij de volgende keer dat de pagina laadt.
Thema: het tabblad Theme is een volledige Material UI-thema-editor met een live
voorbeeld in lichte en donkere modus: de merkkleuren en elke paletkleur per modus, de
lettertypen (elk Google Font) en elke tekststijl, hoekradius, witruimte, schaduwen,
animatiesnelheid, de stijl van knoppen, kaarten en invoervelden, standaardwaarden van
componenten en component-overrides als JSON. Het controleert contrast, start vanaf presets,
importeert een thema uit een andere Material UI-app en exporteert dit als code. Show on the
whole app probeert een concept op elk scherm uit voordat je opslaat. Een workspace die zijn
thema nooit wijzigt, houdt exact de ingebouwde look. In een repository is het thema de sleutel
theme van workspace/branding.json.
Valkuilen: het logo is ook het pictogram van het browsertabblad en het pictogram dat een telefoon voor de app op het startscherm toont. Het startscherm heeft een logo in PNG, JPG of WebP nodig; met een SVG toont het het pictogram van Archestack.
Analytics
Google Analytics op de gepubliceerde app, ingesteld op de pagina Analytics (Configuration). De builder wordt nooit gevolgd.
Gangbare werkwijze: plak de GA4-meet-ID (G-XXXXXXXXXX, in Google Analytics onder Beheer, Gegevensstreams), vink Track visits to the published app aan en sla op. Ask visitors for consent first staat standaard aan: de tag van Google laadt pas nadat een bezoeker de banner heeft aanvaard, zoals de cookieregels van de EU vragen. De tekst van de banner, de knoppen en een link naar je privacybeleid kunnen je eigen zijn.
Events vanuit paginacode: zodra de tag geladen is, kan paginacode eigen events sturen, zoals window.gtag?.("event", "sign_up"). Er wordt niets verstuurd zolang het volgen uit staat of de bezoeker weigerde.
Translations
Key/value-editor voor de UI-strings van de app. Voeg nieuwe locales toe, overschrijf default labels per klant.
Veelvoorkomende workflow: kies een locale, zoek naar een key, typ een nieuwe waarde, sla op. Refresh de adminapp.
Valkuilen: vertalingen worden eenmaal per sessie geladen; gebruikers op een verouderde tab zien geen wijzigingen totdat ze refreshen. Sommige labels (kolomkoppen van BEs) komen uit de BE-config, niet uit translations.
Via-script-aanroepbare API's
Het exacte oppervlak dat scripts kunnen aanroepen. Geverifieerd tegen de source, elke methode hieronder bestaat daadwerkelijk.
Globals
Elk script krijgt deze top-level-variabelen (geen andere namen bestaan in script-scope):
Entity-dynamicExpandoObject. Huidig record. Toegang tot velden metEntity.column_name.Entitymuteren in een Before*-trigger persisteert de wijzigingen als onderdeel van de in-flight write.OldEntity-dynamicExpandoObject. Vorig record (update- + delete-triggers). Read-only.Log-ILogger.Log.LogInformation("..."),Log.LogWarning(...),Log.LogError(...). Schrijft naar serverlogs en de Event Log-entry.Db-DbHelper. Database-toegang: een getypte set per Business Entity, en de ongetypte helpers (zie hieronder).Modules- roep andere Script Modules aan. Elke module is een methode met zijn eigen parameters:await Modules.OrdersOf(customer: "Acme"), gecontroleerd wanneer het script compileert; het resultaat isdynamic.await Modules.CallAsync("OrdersOf", new Dictionary<string, object?> { ["customer"] = "Acme" })roept een module nog altijd aan op naam en geeftTask<object?>terug.Pdf-ScriptPdfHelper. Render een opgeslagen PDF Template op naam:await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() })geeftbyte[]terug;await Pdf.GenerateBase64Async(...)geeft dezelfde payload base64-gecodeerd terug. Parameters worden doorgegeven aan het datascript van de template als string-getypte locals.Ocr-ScriptOcrHelper. Lees de gedrukte tekst uit een afbeelding, in-process, geen externe service en geen API-key:await Ocr.ExtractAsync(byte[] image)geeft eenOcrExtractionterug.await Ocr.ExtractBase64Async(string)neemt dezelfde afbeelding base64-gecodeerd (eendata:image/png;base64,-prefix wordt getolereerd),await Ocr.ExtractTextAsync(byte[])geeft alleen de tekst terug.Doc-ScriptDocHelper. Vind het document in een foto en maak het vlak:await Doc.CropAsync(byte[] image)geeft eenDocumentCropterug.await Doc.CropBase64Async(string)neemt een base64-capture.Files-ScriptFilesHelper. De eigen bestandsmap van de workspace, die de pagina Files beheert:await Files.WriteAsync("invoices/1042.pdf", bytes),await Files.ReadAsync(path),ListAsync(folder),MoveAsync,DeleteAsync. Zie Files.
Er is geen User, Http, Email,
Templates, of enige andere global.
Db - single-row + write-methodes
Alle async, alle op het top-level Db-object:
await Db.GetAsync(string table, object id)- haal een rij op via primary key. Geeftdynamic?terug (ofnullals niet gevonden).await Db.CreateAsync(string table, object values)- insert.valueskan een anoniem object zijn of eenDictionary<string, object?>. Vuurt Before/After Create-triggers. Geeftintterug (het nieuwe ID).await Db.UpdateAsync(string table, object id, object values)- update op PK. Vuurt Before/After Update-triggers.await Db.DeleteAsync(string table, object id)- delete op PK. Vuurt Before Delete-trigger.
Db - fluent query builder
Start met Db.From(table). Keten filters, dan een terminal:
.Where(string column, string op, object? value = null)- voeg een WHERE-clausule toe. Operatoren:=,!=/<>,>,>=,<,<=,LIKE,ILIKE,IN,IS NULL,IS NOT NULL. Keten meerdere.Where(...)-calls voor AND..OrderBy(string column, bool desc = false)- stel ORDER BY in. Slechts een per query toegestaan..Limit(int limit)- maximum rijen. Default is 1000.
Terminals (elk draait de query):
await ...ToListAsync()- geeftList<dynamic>terug.await ...FirstAsync()- geeftdynamic?terug (eerste match of null). Zet de limit tijdelijk op 1.await ...CountAsync()- geeftintterug.
Er is geen SumAsync, MaxAsync,
FirstOrDefaultAsync, of SingleAsync op Db.From. Voor
aggregaties, haal op met ToListAsync() en reduceer in C#, of gebruik de getypte
sets hieronder, die ze allemaal hebben.
Uitgewerkt voorbeeld
// Get a row by ID, query a list, calculate a sum, update.
var order = await Db.GetAsync("orders", 42);
if (order == null) return false;
var items = await Db.From("order_items")
.Where("order_id", "=", order.id)
.OrderBy("id")
.ToListAsync();
decimal totalAmount = 0;
foreach (var item in items) {
totalAmount += (decimal)(item.amount ?? 0);
}
await Db.UpdateAsync("orders", 42, new {
total = totalAmount,
updated_at = DateTime.UtcNow
});
return true; Db - Business Entities als getypte sets
Elke Business Entity is ook een getypte set op Db, met de naam van de entity
zonder spaties: een entity die Service Order heet, is Db.ServiceOrder.
Onder de motorkap is het EF Core, dus heel LINQ werkt (Where, Include,
SumAsync, GroupBy, projecties), met een navigatie voor elke foreign key
en elke Business Entity-join. Kolommen zijn PascalCase-properties met hun echte types:
planned_date is PlannedDate, een DateOnly.
// Every business entity is a typed set on Db, named after the entity.
var since = new DateOnly(2026, 1, 1);
var open = await Db.Order
.Where(o => o.Status == "Open" && o.OrderDate >= since)
.Include(o => o.OrderItems)
.OrderBy(o => o.OrderDate)
.ToListAsync();
foreach (var order in open)
order.Total = order.OrderItems.Sum(i => i.Amount ?? 0);
var customer = await Db.Customer.SingleAsync(c => c.Email == "ada@example.com");
Db.Order.Add(new Order { Customer = customer, Status = "Open" });
// Writes every changed, added and removed row; business events fire as usual.
await Db.SaveChangesAsync();
return open.Count; - Namen. Een tabel waarop geen Business Entity is gebouwd, krijgt een set met de naam van de tabel (
bank_accountisDb.BankAccount). Een foreign-key-kolom zonder zijn_id-suffix is de navigatie:customer_idisCustomer, en de andere kant is het meervoud,Customer.Orders. - Opslaan.
await Db.SaveChangesAsync()schrijft elke toegevoegde, gewijzigde of verwijderde rij weg zoalsCreateAsync,UpdateAsyncenDeleteAsyncdat doen, dus Business Events vuren en audit-kolommen worden gestempeld, en van een gewijzigde rij gaan alleen de kolommen mee die veranderd zijn. Dat gebeurt niet in één transactie: rijen die al opgeslagen waren voordat er iets misging, blijven opgeslagen.ExecuteUpdate,ExecuteDeleteen schrijfoperaties met ruwe SQL worden geweigerd. - Types. Een
timestamp with time zonewordt gelezen als eenDateTimein UTC, en een waarde die je ermee vergelijkt, moet ook in UTC zijn:DateTime.UtcNow. Kolommen van een type dat het model niet mapt (arrays, enums, geometrische types), worden weggelaten; lees die metDb.From. - Altijd actueel. Het model wordt opnieuw opgebouwd telkens wanneer een tabel of een Business Entity wijzigt, dus een verwijderde kolom geeft een compileerfout op de regel die hem gebruikt, in plaats van een fout tijdens het draaien. Een deployment die nog moet draaien, toont vooraf welke getypte scripts daarna niet meer zouden compileren.
Db.From en de methodes hierboven blijven werken naast de getypte sets, en de
scripteditor en VS Code vullen de sets, de kolommen en de navigaties automatisch aan.
Simulation mode
Tijdens trigger-simulatie (de Run Simulation-knop op een Business Event) en in de
Test-tab van een Script Module staat Db.SimulationMode op true.
Schrijfoperaties (CreateAsync, UpdateAsync, DeleteAsync
en SaveChangesAsync) controleren de tabel, de rij en de waarden, leggen vast wat ze
zouden hebben gedaan en schrijven niets weg, dus er vuurt ook geen Business Event. Leesoperaties
zien de echte data. Schrijf-, verplaats- en verwijderacties via
Files worden ook gesimuleerd, en mail en meldingen worden
vastgelegd, niet verstuurd. Dit is wat Run Simulation veilig maakt om tegen echte data te
gebruiken. Al het andere dat het script zelf doet, is echt: HTTP-aanroepen, System.IO,
en alles wat een NuGet-package doet, zoals een verbinding met een
andere database.
Modules
await Modules.OrdersOf(customer: "Acme")- elke Script Module is een methode met de naam van de module zonder spaties ("Orders of" wordtModules.OrdersOf), met de parameters die hij declareert en hun types, de verplichte eerst; een optionele parameter die je weglaat krijgt zijn standaardwaarde. Een verkeerd gespelde naam of een waarde van het verkeerde type is een compileerfout op zijn regel. GeeftTask<dynamic?>terug, dus je leest het resultaat zonder cast, ook een anoniem type. Argumenten uitEntityzijn dynamic: cast ze ((int)Entity.company_id) zodat de aanroep gecontroleerd wordt bij het compileren.await Modules.CallAsync(string moduleName, Dictionary<string, object?>? parameters = null)- roep een andere Script Module aan op naam. Parameters worden getypte variabelen in het script van de aangeroepen module. GeeftTask<object?>terug (wat de module ook teruggaf).
Files - de map van de workspace
Elke workspace heeft op de server een eigen map, die je beheert op de pagina
Files (Operations). Uploads komen daar terecht, en ook alles wat een script
bewaart: een gegenereerde factuur, een export, een getraind model. Niets daarvan staat in een
database, en geen enkel pad komt in de map van een andere workspace. Paden zijn relatief en
gebruiken gewone schuine strepen; een pad met .., een schijf of een link erin wordt
geweigerd.
// A generated invoice, kept in the workspace's own folder.
var pdf = await Pdf.GenerateAsync("Invoice", new Dictionary<string, string> { ["id"] = Entity.orders_id.ToString() });
var path = $"invoices/{DateTime.UtcNow:yyyy}/{Entity.orders_number}.pdf";
await Files.WriteAsync(path, pdf);
// Read it back, or anything an admin uploaded on the Files page.
var rates = await Files.ReadTextAsync("config/vat-rates.json");
foreach (var file in await Files.ListAsync("invoices", recursive: true))
Log.LogInformation("{Path} is {Size} bytes", file.Path, file.Size);
return path; ReadAsync(path)geeft de bytes terug en gooit een fout als het bestand niet bestaat;ReadTextAsyncleest UTF-8-tekst;ReadDataUriAsyncgeeftdata:image/png;base64,…terug, voor een afbeelding in een PDF Template of een mail.WriteAsync(path, bytes)enWriteTextAsyncmaken de mappen aan en vervangen een bestaand bestand. Een schrijfactie lukt helemaal of niet.ListAsync(folder, recursive)toont eerst de mappen; elk item heeftPath,Name,IsFolder,Size,ModifiedUtcenContentType.MoveAsync(from, to)overschrijft nooit;DeleteAsync(path)verwijdert een bestand, of een map met alles erin.PathOf(path)is het absolute pad op schijf, voor een bibliotheek die een pad verwacht.
Bij een proefrun (de knop Test) en bij een testrun bereiken schrijf-, verplaats- en
verwijderacties de map nooit: de run houdt ze voor zichzelf, zodat een net geschreven bestand
teruglezen werkt, en toont ze bij wat hij heeft vastgelegd. De map .archestack bevat
de eigen bestanden van het platform (logo's, pakketiconen, mailbijlagen): scripts mogen hem lezen,
nooit schrijven. Een back-up bevat de map als workspace-files-archief naast de
database-dumps.
Ocr en Doc - een foto lezen
Beide draaien binnen het API-proces. Er is geen OCR-service om te deployen, geen key om te configureren en geen kost per pagina.
OcrExtraction (wat Ocr teruggeeft):
Text- elke herkende regel samengevoegd met newlines, in leesvolgorde.Lines- per regelText,Confidenceen een bounding box:X,Y,Width,Height, plusRightenBottom. Pixels in de bronafbeelding, oorsprong linksboven.Confidence- gemiddelde van de regel-confidences, 0 tot 1.ElapsedMs,IsEmpty.
Gebruik de boxen. Een document lezen betekent een label lokaliseren en dan lezen wat ernaast
staat, en dat kan tekst alleen niet uitdrukken: zoek de regel met "Totaal", neem dan de regel
waarvan het Y-bereik overlapt en waarvan X voorbij de
Right ervan ligt.
DocumentCrop (wat Doc teruggeeft):
Found- of er echt een contour gedetecteerd is. Controleer dit eerst.Image- PNG-bytes van de vlakgemaakte pagina, of de originele bytes ongewijzigd wanneerFoundfalse is. Een foto die nooit is bijgesneden leest nog steeds; een foto die naar de verkeerde vierhoek is bijgesneden niet, dus de detector weigert liever dan te gokken.Corners- de contour in de bronfoto, linksboven, rechtsboven, rechtsonder, linksonder.Width,Height,ElapsedMs.
De twee combineren in de voor de hand liggende volgorde, en omdat een mislukte crop het
origineel teruggeeft, doet ze aan elkaar knopen altijd iets: var crop = await
Doc.CropAsync(photo); var scan = await Ocr.ExtractAsync(crop.Image);
Drie limieten om rekening mee te houden. Het meegeleverde model is PP-OCRv5
latin, dus westerse alfabetten en cijfers, geen CJK. Het is geen PDF-lezer, een PDF-pagina moet
eerst naar een afbeelding gerasterd worden. En dit is OCR, geen document-begrip: regels omzetten
naar leverancier, factuurnummer en totaal is logica die jij schrijft, en die zal soms fout
zitten, dus controleer line.Confidence en stuur lage waarden naar een mens in
plaats van ze rechtstreeks naar een tabel te schrijven.
Let op de script-timeout. Een volle pagina duurt een seconde of twee en de eerste aanroep in het
proces telt er ongeveer een seconde bij voor het laden van de modellen. De timeout van tien
seconden onderbreekt geen await, dus een script dat meerdere pagina's leest geeft
helemaal niets terug in plaats van vroeg te falen. Doe meerdere pagina's in een Scheduled Event,
een pagina per run.
Template-expressies (Business Event-actions)
Actie-configuratie gebruikt {{ ... }}-haakjes. De volledige lijst geldige tokens:
{{ Entity.column_name }}/{{ Entity.id }}{{ OldEntity.column_name }}{{ now() }}/{{ getdate() }}- ISO 8601 UTC-datetime{{ today() }}- datum-string{{ guid() }}/{{ newid() }}- verse GUID{{ year() }},{{ month() }},{{ day() }},{{ timestamp() }}- Unix-seconden{{ empty() }},{{ null() }}- Aggregates binnen een join-context:
{{ SUM(column) }},{{ AVG() }},{{ COUNT() }},{{ MIN() }},{{ MAX() }} - Rekenkunde:
{{ Entity.quantity * Entity.price }}- geevalueerd na substitutie viaDataTable.Compute()
Er is geen {{ user.email }} of andere user-scoped token.