Skip to content

Workspace provisioning ​

Every company-layer workspace Tellia ships is data, declared in apps/agri-backend/src/workspaces/definitions/ and applied by a queue-driven service. There is one provisioning path; the seeders that used to do this (WorkspaceInitializationService, AggKnowledgeService.initializeWorkspaces, the tellia:fields DB template) are gone.

Layers, and what this page covers ​

LayerOwnerWhere
systemTell-ia's global catalogues (EPPO crops, pests, …)workspaces/system/*.ts, applied by SystemWorkspaceBootstrapProcessor
companythis page β€” what a tenant getsworkspaces/definitions/*.ts, applied by CompanyWorkspaceBootstrapProcessor
integration / adapter-ownedFieldX, ICSintegrations/fieldx/*, partner-provisioning/ics/*
templateadmin-authored blueprintsadmin/workspace-templates

Adding or changing a definition ​

One file per workspace, one export, validated at import:

ts
export const TIMESHEET_DEFINITION = CompanyWorkspaceDefinitionSchema.parse({
  key: 'tellia:timesheet', // tellia:<slug>, the identity β€” never rename
  name: 'Timesheet', // English; what the tenant sees
  icon: 'clock', // WorkspaceIcon, i.e. a shared registry name
  description: '…',
  isResources: false, // true = Reference workspace, false = Record workspace
  order: 13, // unique across definitions
  activities: [COMPANY_ACTIVITY_CODES.GROWER],
  tableHeader: [
    /* scalar columns */
  ],
  references: [
    // resolved to workspace ids at provisioning time
    { name: 'workerName', targetKey: 'tellia:worker', description: '…' },
  ],
} satisfies CompanyWorkspaceDefinitionInput);

Then add it to definitions/all.ts. DEFINITION_HASHES is derived, so nothing else to update.

Company twins of a system catalogue (crops, pests, pesticides, …) do not repeat the catalogue. They call fromSystemDefinition({ key, order, activities, description }), which reads SYSTEM_WORKSPACE_DEFINITIONS, follows that entry's declared parent to fold in the base columns, and reuses its icon and label.

definitions.invariants.spec.ts enforces: unique keys and orders, icons present in the shared registry, reference targets that exist and are provisioned for the same activities, no duplicate column names, a name column on every Reference workspace, and every activity having at least one Record workspace.

Column names: the one non-obvious rule ​

Two different rules apply, and they are load-bearing:

  • Hand-authored columns are camelCased. CompanyWorkspaceDefinitionSchema runs normalizePropertyName, so n_pct is stored as nPct and related_crops as relatedCrops. This matches what the old seeder wrote (it parsed through the same schema), so existing tenant records still line up with their columns.
  • Columns taken from a system catalogue keep the catalogue's names. The system bootstrap does not parse through that schema, so tellia:crops stores scientific_names. Federation unions columns by name across layers, so the company twin must not camelCase them. fromSystemDefinition restores the source names after validating.

definitions.invariants.spec.ts compares company twins against the catalogue, and definition-i18n.spec.ts checks the i18n files use the stored names β€” otherwise a translation silently falls back to English.

What provisioning does to an existing workspace ​

CompanyWorkspaceProvisioningService.provisionCompany runs two passes: scalar columns first, so every key has an id, then reference columns resolved from a map built for that company only (a reference can never point at another tenant's workspace).

Workspace.provisioning records what the seeder last wrote β€” defHash, locale, name, description, and per-column values. That is what makes a re-provision safe:

Behaviour
Row matches its definition hash and localeskipped, no write, no audit entry
Tenant renamed the workspacerename kept
Tenant edited a column label or descriptionedit kept
Tenant tuned required, sortable, filterable, width, aikept
Column type, arrayType, workspaceIddefinition-owned, always refreshed
Tenant added a columnkept, after the definition's columns
Tenant deleted a definition columnstays deleted, never resurrected
Legacy row with no provisioning subdocfirst pass keeps every value the row already holds and records the definition's own values as the baseline; a later re-provision then refreshes anything that matched the definition and leaves anything that diverged (it now reads as a tenant edit)
Legacy row whose name is not a spelling of the definition'skept forever. On a row we have never written, the only rename allowed is a spelling fix (case, trailing plural: "Crop" β†’ "Crops"). Anything further β€” "Training Session Log", or "CΓ©pages" on a tenant whose locale says en β€” is theirs
Row with an adapterKey (FieldX, ICS)skipped β€” that seeder owns it
Row with no key, or a pre-tellia: key (reminders, generic)left alone. Re-keying a tenant's row is WorkspaceKeyMigrationService's job, not provisioning's; the definition is created alongside it under the canonical key
Row with the definition's name under another tellia: keyskipped, counted as nameConflicts

Nothing is ever deleted. Removing a definition stops maintaining those workspaces; it does not remove them from tenants who have them.

The queue ​

company-workspace-bootstrap, concurrency 1, one job:

JobTriggerDoes
provisioncompany creation, admin re-initone company

There is deliberately no sweep. A definition change reaches an existing tenant only when someone asks for it β€” never on a timer. Provisioning runs where WorkspaceInitializationService ran: once, at company creation. Rolling a definition change out to tenants who already exist is a migration someone runs on purpose, not a background job, because it would otherwise hand ~5 workspaces per tenant to companies that never asked for them.

Company creation only enqueues, so it never depends on ~20 workspace writes.

translate (its own queue) fills missing definition translations; trigger it from POST /admin/acquisition/workspace-definitions/translate.

Translations ​

Strings resolve checked-in i18n JSON β†’ generated cache β†’ English:

  1. src/i18n/translations/<locale>.json under workspaces.<scope>.…, where <scope> is the key without tellia:. Hand-written, highest quality.
  2. workspace_definition_translations, unique on (defKey, defHash, locale). Keyed by the definition hash, so changing a definition asks for fresh strings rather than serving stale ones. Platform-wide: translated once, reused by every tenant.
  3. The English definition.

Resolution never throws. A definition shipped today serves English until the translate job lands, then the next re-provision applies the translation (the stored provisioning.locale and values no longer match, so the row is refreshed).

While a locale is falling back, isEnglishFallback is set and provisioning will not write the name or description of a workspace that already exists β€” otherwise a French tenant's "Parcelles" would become "Fields" for the cycle before the fill runs, and the seven company twins of a system catalogue (crops, pests, …) have no translation at all, so it would be permanent. New workspaces are still created with the English strings.

POST /admin/acquisition/workspace-definitions/translate with { locales?, force? } runs a pass by hand β€” a new locale, or force after a bad batch.

Operating it ​

logql
{app="tellia-agri-backend"} |= "Company workspaces provisioned"
{app="tellia-agri-backend"} |= "Provisioned a definition on request"
{app="tellia-agri-backend"} |= "Workspace name already taken under another key"

A re-provision after a definition change writes once per drifted workspace, then the hash guard makes further runs free.