Kernconcepten

Het mentale model achter alles

Archestack heeft een klein aantal kernconcepten die overal in het product opduiken. Elk concept bestaat om een probleem op te lossen dat je anders met de hand zou oplossen, telkens opnieuw, op subtiel verschillende manieren. Vijftien minuten hier doorbrengen bespaart je uren rondzoeken. Elke sectie hieronder introduceert een concept, geeft een uitgewerkt voorbeeld en wijst op de fouten die nieuwe gebruikers maken.

Het Schema is de bron van waarheid

Alles begint met het Schema, een JSON-beschrijving van elke tabel, kolom, relatie en index in je applicatie. Je schrijft geen SQL met de hand. Je ontwerpt tabellen in de visuele Schema Designer, en Archestack zet dat bij deploy om in echte PostgreSQL-tabellen.

Waarom het bestaat

Een traditioneel ERP is een wirwar van tabellen die in tien jaar organisch is gegroeid. Archestack draait het model om: het schema is een geversioneerd artefact dat je expliciet bewerkt, net als broncode. Elk ander gereedschap in het platform - Business Entities, Pages, Events, Scripts - leest uit het schema. Als een kolom niet in het schema bestaat, weet geen enkel ander gereedschap ervan af.

Hoe het in de praktijk werkt

  • Auto-save versus deploy. Bewerken in Schema Designer slaat je wijzigingen automatisch op (een indicator verschijnt linksonder). Opslaan verandert de database niet, dat gebeurt pas wanneer je vanuit Database Deployments een deployment aanmaakt en op Deploy klikt.
  • Generated SQL is bewerkbaar. De Generated SQL-tab van de deployment toont de migratie-SQL die het platform heeft gegenereerd: CREATE TABLE, ALTER TABLE, etc. Je kunt hem lezen voordat je deployt, en je kunt hem rechtstreeks bewerken als de automatisch gegenereerde versie niet helemaal klopt.
  • Pre/post-hooks. Een deployment kan pre- en post-deployment-scripts dragen (PostgreSQL), handig om een nieuwe NOT NULL-kolom te vullen, of om een index in een gecontroleerde volgorde te herbouwen.
  • Geschiedenis is permanent. Elke deployment wordt gelogd met zijn status (Draft / Executing / Succeeded / Failed) en de SQL die is uitgevoerd.

Uitgewerkt voorbeeld

Je voegt een priority_score-kolom toe aan deal. In Schema Designer selecteer je de tabel, open je de Columns-tab, typ je de naam in het veld "column_name", kies je INTEGER uit het Type-dropdown en klik je op Add Column. De auto-save-indicator toont kort "Auto saving..." en daarna "Saved". Klik vervolgens op Deploy in de werkbalk, je belandt op een nieuwe deployment-configuratiepagina. Open de Generated SQL-tab:

ALTER TABLE "deal" ADD COLUMN "priority_score" INTEGER;

Klik rechtsboven op Deploy, klaar. De kolom bestaat nu. Totdat je hem ook toevoegt aan de Business Entity van deal (volgend concept), ziet geen enkele pagina of event hem. Die scheiding is opzettelijk: schema-wijzigingen zijn goedkoop, ze blootstellen is de bewuste vervolgstap.

Valkuilen

  • Een kolom hernoemen droppet en hermaakt hem. De plan-generator kan je gedachten niet lezen. Als je hernoemt, bewerk dan de SQL op de Generated SQL-tab om RENAME COLUMN te gebruiken, of doe een deployment in meerdere stappen: voeg de nieuwe kolom toe, schrijf een post-script om de data te kopieren, en drop pas in een vervolg-deployment de oude kolom.
  • Een NOT NULL-kolom toevoegen aan een niet-lege tabel zal falen. Geef ofwel een default mee, of voeg hem nullable toe, vul hem, en wijzig hem in een tweede deployment naar NOT NULL.
  • Foreign keys beperken het delete-gedrag. Gebruik de On Delete-dropdown op elke relatie bewust, de keuzes zijn CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION.
  • Snake_case is de conventie. Het veld "Table name" van Schema Designer hint er zelfs naar ("Lowercase with underscores recommended"). De tools werken ook met PascalCase, maar elk codevoorbeeld op deze site gaat uit van snake_case voor tabel- en kolomnamen.

Business Entities zijn de gecureerde view

Een ruwe tabel (customer) is zelden wat een eindgebruiker wil zien. Die wil "klant met het totaal van zijn laatste order" of "voertuig met de naam en het telefoonnummer van de toegewezen monteur". Een Business Entity (BE) is een gecureerde view op een brontabel plus joinkolommen uit gerelateerde tabellen. Pages worden gebouwd op BEs, niet op ruwe tabellen.

Waarom het bestaat

Twee redenen. Ten eerste zou tabellen joinen in elke pagina repetitief en inconsistent zijn, verschillende pagina's zouden dezelfde tabellen op net iets andere manieren joinen, en het gedrag zou uit elkaar gaan lopen. De BE centraliseert de join-logica. Ten tweede wil je vaak meerdere "views" op dezelfde brontabel voor verschillende doelgroepen: de sales-view op customer toont omzet en laatste contact; de support-view toont tickets en SLA-overschrijdingen. Verschillende pagina's verwijzen naar verschillende BEs, allemaal onderbouwd door dezelfde rij in customer.

Anatomie

  • Master Table - de tabel waarop de BE is verankerd. Elke BE heeft er precies een. De primary key van de BE is de primary key van de master table.
  • Label Column - een kolom waarvan de waarde een record identificeert in lijsten en pickers (meestal name, title, of vergelijkbaar). Wordt gekozen wanneer je de BE aanmaakt.
  • Joins - toegevoegd via de Add Join-knop. Elke join heeft een join-type (INNER / LEFT / RIGHT), een doeltabel, bron/doel-kolommen, en een lijst van welke doelkolommen naar boven worden gehaald.
  • Geaggregeerde kolommen - schakel Aggregate Mode aan op een join, kies dan per kolom een aggregatiefunctie: COUNT, SUM, AVG, MIN, MAX, COUNT DISTINCT. "Aantal openstaande contacten op deze Company" is een typische geaggregeerde kolom.

Uitgewerkt voorbeeld

Master table: vehicle. Native kolommen die je toont: vin, make, model, year. Join met customer via vehicle.owner_id naar customer.id, en haal customer.full_name naar boven als owner_name. Tweede join in aggregate mode: aantal work_order-rijen waar vehicle_id overeenkomt en status != 'Closed', naar boven gehaald als open_work_order_count.

Een pagina die aan deze BE is gebonden rendert een enkele rasterrij die eruitziet als een plat feit, ook al haalt hij gegevens uit drie tabellen, en doet dat consistent over het hele platform.

Valkuilen

  • BEs zijn standaard geen schrijfgrens. Opslaan vanuit een aan een BE gebonden formulier schrijft naar de master table. Gejoinde kolommen zijn meestal read-only, om owner_name in het bovenstaande voorbeeld te bewerken navigeer je naar de Customer.
  • Gebruik Run Preview royaal. De knop met label Run Preview in de BE-editor draait de join-configuratie en toont echte rijen. Als een joinkolom leeg terugkomt, is je relatie of kolomkoppeling fout geconfigureerd.
  • Reik niet over te veel niveaus. Een BE die drie tabellen diep joint begint traag aan te voelen op grote datasets. Als je vier of vijf tabellen joint, is dat een hint dat je een custom Script Module of een database-view wilt.

Pages worden geconfigureerd, niet gecodeerd

Een Page is een runtime-UI gebouwd uit een of meer Business Entities. Je configureert ze in de Page Editor door een naam, een route (bijv. /companies), een Business Entity-binding en een Published-toggle in te stellen. Het platform genereert automatisch secties uit de BE-kolommen; van daaruit tune je de indeling.

Anatomie

  • Visual-tab - de WYSIWYG-layout-editor. Secties, velden, tabs, actieknoppen.
  • Overview-tab - lijst/grid-configuratie: welke kolommen verschijnen, standaard sortering, filters.
  • Create-tab - het formulier voor een nieuw record. Wijkt vaak iets af van het detailformulier (minder velden, andere defaults).
  • Entities-tab - extra BE-bindingen (de pagina kan naar meer dan een BE verwijzen voor tabs en gerelateerde grids).
  • Events-tab - event-triggers met paginascope.
  • JSON-tab - ruwe JSON voor power users.

Veld-widgettypes

Voor elk veld in het detailformulier kies je een Type uit een Select-control. De daadwerkelijke opties:

  • Text - invoer van een regel
  • Textarea - invoer over meerdere regels
  • Number - numerieke invoer
  • Date - datumpicker
  • Select - dropdown (gebruik dit voor foreign-key-velden en enum-achtige kolommen; voor FK-velden zet je de Entity-autocomplete op de gerefereerde BE zodat gebruikers op label kiezen)
  • Checkbox - boolean
  • Email - tekstinvoer met e-mailvalidatie

Voor elk veld configureer je ook Label, Placeholder, Required, Read Only en Span (raster-kolombreedte).

Tabs en gerelateerde grids

Het detailformulier van een pagina ondersteunt tabs. Gebruik de knop Add Tab om er een toe te voegen. Binnen een tab kun je een RelatedGrid-sectie toevoegen die rijen uit een andere BE toont, gefilterd via een join (bijv. alle contact-rijen waar company_id = current company's id). Zo bouw je master-detail-pagina's als "Company met Contacts en Deals" zonder code.

Publiceren

Elke pagina heeft een Published-schakelaar in de bovenste header. UIT = concept (alleen jij ziet je wijzigingen in de editor); AAN = de gepubliceerde pagina verschijnt onder Published Pages in de sidebar en is wat eindgebruikers zien wanneer ze naar de route navigeren.

Frontend Templates: wanneer "configureer, niet coderen" niet genoeg is

Soms zijn de geconfigureerde templates niet genoeg, dan wil je een custom widget, een ongebruikelijke indeling, of een specifieke visualisatie. Frontend Templates laten je een stuk TSX schrijven dat in de Page Editor beschikbaar komt als herbruikbare component. Ze zijn packagebaar en staan naast de rest van je configuratie. Zie Referentie, Frontend Templates.

Valkuilen

  • Vergeten om de Published-schakelaar om te zetten? De pagina verschijnt niet in de sidebar en eindgebruikers die naar de route navigeren krijgen een 404. Makkelijk over het hoofd te zien omdat de editor het niet zichtbaar afdwingt.
  • Breedte doet ertoe. Als je BE 25 kolommen heeft, is het grid onbruikbaar op een laptop. Verberg kolommen met weinig waarde; ze blijven querybaar, ze worden alleen niet weergegeven.
  • Tabs triggeren aparte queries. Een pagina met vijf tabs doet extra queries wanneer je een detailrij opent. Meestal prima, soms een performance-zorg op grote datasets.

Business Events laten data reageren

Een Business Event kijkt naar een Business Entity voor wijzigingen en draait actions wanneer aan de conditions is voldaan. Het zijn de "wanneer X gebeurt, doe Y"-regels die een passieve database in een werkende applicatie veranderen.

Trigger-timings

De daadwerkelijke opties wanneer je een event aanmaakt (je kunt er meerdere aanvinken):

  • BeforeCreate - vuurt voordat een nieuw record wordt opgeslagen. Het script kan Entity muteren en de wijzigingen worden mee gepersisteerd.
  • BeforeUpdate - vuurt voordat een bestaand record wordt opgeslagen. Zelfde mutatiegedrag.
  • AfterCreate - vuurt nadat een nieuw record is gecommit.
  • AfterUpdate - vuurt nadat een bestaand record is gecommit.
  • BeforeDelete - vuurt voordat een record wordt verwijderd.
  • InitialValue - vuurt wanneer een nieuw formulier wordt geopend; zet standaard veldwaarden.
  • OnSchedule - gevuurd door Quartz op een cron-schema (zie Scheduled Events).
  • Manual - expliciet gevuurd door een actieknop op een pagina.

Actietypes

De daadwerkelijke actietypes (RuleActionKind):

  • ExecuteScript - draai een C#-script. De meest flexibele actie; hier grijp je naar wanneer je een veld wilt zetten, een berekening wilt doen, een API wilt aanroepen, iets op maat. Het script krijgt Entity (muteer het om het record te wijzigen), OldEntity, Log, Db, Modules.
  • Validate - draai een C#-script dat bool teruggeeft. true betekent dat de validatie faalt en het opslaan wordt geblokkeerd met het geconfigureerde foutbericht. Markeert optioneel specifieke kolommen.
  • BlockOperation - stop de operatie hard met een bericht. Geen script.
  • CreateEntity - voeg een record toe aan een andere BE. Veldwaarden ondersteunen template-expressies.
  • UpdateEntity - update records in een andere BE die aan een conditiefilter voldoen.
  • DeleteEntity - verwijder records die aan een conditie voldoen (weigert te vuren zonder conditie, vangnet).
  • SendEmail, SendWebhook, PublishEvent - in het schema gedefinieerd als toekomstige uitbreidingen; momenteel gelogd en overgeslagen.

Template-expressies

Actie-configuratie accepteert template-expressies tussen {{ ... }}-haakjes. De daadwerkelijke tokens:

  • {{ Entity.column_name }} - veld van het huidige record ({{ Entity.id }} werkt ook)
  • {{ OldEntity.column_name }} - vorige waarde (alleen update-triggers)
  • {{ now() }} of {{ getdate() }} - ISO 8601 UTC-datetime
  • {{ today() }} - datum-string
  • {{ guid() }} of {{ newid() }} - nieuwe GUID
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}
  • Aggregates binnen een join-context: {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Rekenkunde: {{ Entity.quantity * Entity.price }} - geevalueerd na substitutie

Let op: er is geen {{ user.email }}-token, scripts en template-expressies hebben geen toegang tot de huidige gebruiker. Als je de identiteit van de gebruiker in een regel nodig hebt, sla die dan bij het inserten op in het record via de front-end en lees hem van daaruit.

Uitgewerkte voorbeelden

  • Normaliseer een veld bij het opslaan. Trigger: Before Update op Deal. Geen condities. Actie: Execute Script met Entity.title = ((string)Entity.title)?.Trim(); om overtollige spaties te verwijderen. (Voor created_at / updated_at / created_by / updated_by heb je geen regel nodig, het platform stempelt die automatisch.)
  • Blokkeer dat deals met een laag bedrag als Won worden gemarkeerd. Trigger: Before Update op Deal. Conditie: stage = 'Won' AND amount < 100. Actie: Block Operation met bericht "Deals onder 100 euro kunnen niet als Won worden gemarkeerd, log ze als Lost of verwijder ze."
  • Maak een Note aan wanneer een Customer wordt gemarkeerd. Trigger: After Update op Customer. Conditie: flagged = true. Actie: Create Entity op note met title = "Customer flagged at {{ now() }}", body = "Auto-generated for {{ Entity.full_name }}".

Simuleer voordat je opslaat

De trigger-editor heeft een Simulate-tab met een knop met label Run Simulation. Kies een echt record en het platform draait de condities en acties erop af zonder wijzigingen te persisteren, schrijfoperaties worden uitgevoerd in een transactie die wordt teruggedraaid. Het outputpaneel toont welke condities matchten en wat elke actie zou hebben gedaan. Gebruik het voordat je een trigger op productiedata aanzet.

Valkuilen

  • Recursieve triggers. Een After Update-trigger die Update Entity gebruikt om hetzelfde record te updaten zal zichzelf opnieuw vuren. Gebruik in plaats daarvan Before Update + Execute Script + Entity.field = ..., dat muteert de in-flight write in plaats van een nieuwe te starten.
  • Volgorde doet ertoe tussen triggers. Meerdere triggers op hetzelfde event vuren op prioriteitsvolgorde. Als het gedrag van de volgorde afhangt, stel prioriteiten expliciet in.
  • Uitgeschakelde events verschijnen toch in de lijst. De Enabled-schakelaar staat los van de trigger-config, controleer dat hij aan staat voor je test.

Script Modules geven je een achterdeur

Voor alles wat niet als een simpele conditie + actie kan worden uitgedrukt - een complexe korting berekenen, een externe API aanroepen, een slug genereren, een database-operatie in meerdere stappen doen - schrijf je een Script Module: een klein stukje C# (runtime gecompileerd door Roslyn) dat je kunt aanroepen vanuit een Business Event, vanuit een Scheduled Event, of vanuit de front-end.

Waar scripts toegang toe hebben

Elk script krijgt deze globals (er bestaan geen andere namen in script-scope):

  • Entity - het huidige record (in trigger-contexten) als dynamic. Toegang tot velden met Entity.column_name. Entity muteren in een Before*-trigger is hoe je "een veld zet".
  • OldEntity - het vorige record (update- en delete-triggers). Read-only.
  • Log - een ILogger voor diagnostiek. Log.LogInformation("...") schrijft naar de serverlogs en de Event Log-entry.
  • Db - de database-helper. Zie hieronder.
  • Modules - roep andere Script Modules aan: await Modules.CallAsync("OtherModule", new Dictionary<string, object?> { ["param"] = value }).
  • Pdf - render een opgeslagen PDF Template op naam: await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) geeft de gerenderde byte[] terug. Pdf.GenerateBase64Async(...) geeft dezelfde payload base64-gecodeerd terug.

Er is geen User-, Http- of Email-global.

Hoe een script eruitziet

// Parameters declared in the UI: customer_id (int)
var customer = await Db.GetAsync("customer", customer_id);
if (customer == null) return new { ok = false, error = "Customer not found" };

var orders = await Db.From("sales_order")
    .Where("customer_id", "=", customer_id)
    .Where("status", "=", "Closed")
    .ToListAsync();

decimal totalRevenue = 0;
foreach (var o in orders) totalRevenue += (decimal)(o.total ?? 0);

return new { ok = true, customer = customer.name, revenue = totalRevenue };

De meeste scripts zijn 5-30 regels lang. Je hebt geen .NET-expertise nodig, je hebt basale C#-control flow en de Db-helper nodig. IntelliSense staat aan, en na een toewijzing vanuit Db.GetAsync("customer", ...) kent de editor de kolommen van die variabele en biedt completions aan.

Wanneer grijp je naar een script

  • Berekeningen die meerdere tabellen omspannen (de lifetime value van een klant, het totaal van de reparatiehistorie van een voertuig).
  • Externe API's aanroepen (geocoding, betaalproviders, sms-gateways).
  • Bulkoperaties getriggerd vanuit een Scheduled Event (nachtelijke herberekening, wekelijkse digest).
  • Data teruggeven aan de front-end die te eigen is voor een Business Entity.
  • Alles waarvoor je anders zes Business Events aan elkaar zou ketenen, meestal een teken dat je een script wilde.

Valkuilen

  • De Db-helper is async. Altijd await. Het vergeten compileert wel, maar geeft een Task terug, niet de data.
  • Er is geen FirstOrDefaultAsync of SumAsync. De terminals zijn ToListAsync(), FirstAsync(), CountAsync(). Voor een som: haal de rijen op en sommeer in C#.
  • Where neemt drie argumenten - column, operator, value. Operatoren zijn onder meer =, !=, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL.
  • Update is een top-level call: await Db.UpdateAsync("table", id, new { field = value }), niet geketend op een Where.
  • Trial-scripts draaien in dezelfde sandbox als productie. Ga er niet vanuit dat "het is maar een trial" ruimere limieten betekent, je script kan Postgres nog steeds laten zweten.

Scheduled Events draaien op een klok

Een Scheduled Event is hetzelfde Event Trigger-systeem met de OnSchedule-timing, een Quartz cron-expressie triggert de regel op een terugkerende basis in plaats van als reactie op een datawijziging. Gebruik het voor nachtelijke samenvattingen, dagelijkse data-refreshes, wekelijkse opschoning, maandelijkse rapporten.

Cron-formaat

Quartz-stijl cron met 6 of 7 velden (seconde minuut uur dag-van-maand maand dag-van-week [jaar]). Enkele veelvoorkomende patronen:

  • 0 0 3 * * ? - elke dag om 03:00.
  • 0 0 9 ? * MON-FRI - weekdagen om 09:00.
  • 0 */15 * * * ? - elke 15 minuten.
  • 0 0 0 1 * ? - de eerste van elke maand om middernacht.

Tijden zijn servertijd (UTC op de productie-stack). De Scheduled Events-pagina toont het volgende moment waarop hij vuurt, zodat je voor het opslaan een sanity check kunt doen.

Valkuilen

  • Langlopende jobs houden hun slot vast. Als een job 25 minuten duurt en de cron elke 15 minuten staat, wordt de volgende run overgeslagen, Quartz zal niet twee instanties van dezelfde job tegelijk vuren.
  • Mislukte runs worden niet opnieuw geprobeerd. Ze loggen naar Event Logs met de exception. Voeg expliciete retry-logica toe aan je script als je dat nodig hebt.

Packages bundelen config voor promotie

Zodra je iets hebt gebouwd, een set tabellen, BEs, pages, events en scripts, wil je het verplaatsen van je trial naar een echte omgeving, of het delen met een andere partner. Een Package is een ZIP van geselecteerde configuratie-objecten (met cascading: een pagina opnemen trekt automatisch zijn BE mee, die zijn tabellen meetrekt) die je kunt exporteren en importeren.

Cascading uitgelegd

Je voegt de top-level-objecten toe die je belangrijk vindt (meestal pagina's). De package-builder loopt door de afhankelijkheidsgraaf en voegt alles toe wat die objecten nodig hebben: BEs waaraan de pagina's binden, tabellen waar de BEs naar verwijzen, Frontend Templates die de pagina's embedden, Script Modules die de events aanroepen. Je ziet de volledige gecascadeerde lijst voor je exporteert en kunt alles uitvinken wat je wilt weglaten.

Seed-data verschepen

Packages kunnen optioneel rij-data bevatten, handig voor het verschepen van default lookup-tabellen (landen, valuta, status-enums) of demo-data. Vink Include data aan bij export. Bij import worden de rijen geupsert op primary key.

Valkuilen

  • Schema-deltas worden bij import niet automatisch gedeployed. Als de importerende omgeving tabellen mist, faalt de import. Deploy eerst het schema, importeer dan de package.
  • Identifiers zijn op naam, niet op ID. Een BE genaamd "customer" in de bron-omgeving bindt aan een BE genaamd "customer" in de bestemming, niet aan hetzelfde numerieke ID. Hernoemingen aan beide kanten breken de link.

Business Units bepalen wie wat ziet

Voor multi-tenant- of multi-team-setups zijn Business Units Keycloak-groepen die kunnen worden toegewezen aan specifieke resources. Een gebruiker in Business Unit "Garage A" kan alleen Pages, BEs en Triggers zien die aan Garage A zijn toegewezen.

Hoe het werkt

Elke gebruiker hoort bij een of meer BUs (beheerd in User Management en Business Units). De front-end houdt de actieve BU van de gebruiker bij en zet de X-Business-Unit HTTP-header op elk verzoek. De backend gebruikt die header om lijst-endpoints te filteren, alleen resources die aan de actieve BU zijn toegewezen komen terug.

Wanneer wel (en wanneer niet)

  • Gebruik het wanneer verschillende teams of klanten hun eigen plak van hetzelfde platform nodig hebben, de monteurs van Garage A mogen Garage B's work orders niet zien.
  • Gebruik het niet als permissiesysteem. BUs filteren zichtbaarheid, geen autorisatie. Rollen (admin / owner / editor / user) regelen wie wat mag doen.
  • Gebruik het niet voor row-level security binnen een enkele tenant. Dat is een taak voor filters op de Business Entity, niet voor een BU.

Waar dit naartoe gaat

Met deze concepten in de hand volgt de rest van de docs vanzelf. De first-app-tutorial gebruikt elk concept op deze pagina. De Referentie gaat een niveau dieper in op elk gereedschap: waar te klikken, waar op te letten, wanneer ernaar grijpen.