Repository navigation
v5.2.0
5.2 is a minor release. Workflows can fire on a cron schedule, create entries, move them through their lifecycle and publish to outside services. Fields can be computed. Saved queries group, count and sum in the database. Six new modules ship (Contacts, Notifications, Comments, Scheduling, Reports, Payments) alongside Documents, Qr, CdnPurge and build triggers, and core can reserve against a counter. The admin API contract moves from 9 to 11. The delivery contract stays at 9. The Resend email module moved to the private feed.
Upgrade
From 5.1.x. A 5.0 install upgrades to 5.1 first.
- Take a backup and check it restores (
docs/backup-and-restore.md). - Stop the 5.1 API. Every file below is applied with the API stopped.
- Run
db-migratewith the 5.2.0 image, thendb-assert.FROM_VERSION=5.1.0 scripts/upgrade-check.sh --listprints each file as apsqlline in the order CI applies them. Undermigrations/5.2.0/:- Core, first:
580-stock-reservations.sql(the stock counter, hold and movement tables, empty),898-accounts.sql(the accounts tables),910-inbound-webhook-events.sql,action-link-uses.sql,magic-link-tokens.sql,outbox-messages.sql,pending-builds.sql,signed-link-uses.sqlandworkflow-schedule-claims.sql(all new, empty tables). - Module, after the core files; a host without that module skips its file:
253-comments.sql(BarakoCMS.Comments),585-documents.sql(BarakoCMS.Documents),booking-resources.sql(BarakoCMS.Booking),contacts.sql(BarakoCMS.Contacts),notifications.sql(BarakoCMS.Notifications),payments-attempts.sql,payments-events.sqlandpayments-payable-types.sql(BarakoCMS.Payments),report-definitions.sql(BarakoCMS.Reports) andscheduled-entries.sql(BarakoCMS.Scheduling).
- Core, first:
- Check the defaults. Delivery response caching (
Delivery:ResponseCache:Enabled), signed action links (Workflows:ActionLinks:Enabled) and email delivery of notifications are off until you set them. The stock hold sweep (Stock:HoldSweep:Enabled) is on by default and does nothing until a counter has a hold. - Start 5.2.0.
Breaking HTTP changes. ApiContract.Version moves from 9 to 11. Admin 10: POST /api/stations answers 400 when the request lists a platform capability. Admin 11: the Resend settings (Resend:ApiKey, Resend:From, RESEND_API_KEY) are no longer read, so move to Email:ApiKey and Email:From. ApiContract.DeliveryVersion stays at 9. Module contracts: BarakoCMS.AI HttpContractVersion 1 to 2, BarakoCMS.Pages 2 to 3. New modules start at 1.
Package changes. BarakoCMS.Abstractions 5.2.0 adds IBarakoModule.ContentPermissions and the package identity members (PackageId, PackageVersion, Category, Description, Glyph) as default members, IContentLifecycleRunner gains an overload that takes a ContentTransition, and IFileStore gains FindForRunAsync and OpenForRunAsync. A module built against 5.1 still loads. Nothing is removed from the package surface.
Rolling back to 5.1. Stop 5.2, run the rollback lines --list prints, newest first, then remove each rolled-back file's ledger row with db-migrate --forget <key> and start 5.1.x. Every file above has a rollback file under migrations/5.2.0/ named rollback-<file>. Rolling back drops the tables those files created, so their rows are lost: stock counters and holds, accounts, inbound webhook events, action and signed link uses, magic link tokens, outbox messages, pending builds, workflow schedule claims, and each module's comments, documents, bookings, contacts, notifications, payment attempts, report definitions and scheduled entries. A workflow set to fire on Schedule stops firing on 5.1.
Added
- A user has a display name.
GET /api/mereturnsdisplayName, which is the username until one is set.PUT /api/me/profilewith{ "displayName": "..." }sets it (up to 100 characters, no control characters); null or blank clears it.User.DisplayNameis a new optional property. - A module can declare default content rights for the types it seeds.
IBarakoModule.ContentPermissions(a default member, so existing modules compile) takes aContentPermissionDefaults, and the host applies it after the module'sSeedAsync. A role gets the declared actions only if it has no permission entry for that type yet, so a restart is not a write and rights an admin changed are never overwritten. A module that declares nothing behaves as before. - Collection syncs can treat an empty answer as complete, and refuse a partial body.
emptyIsComplete(witharchiveMissing) lets a run that read no items archive every entry the
sync owns, for a source that really can be empty.incompleteWhen(pathandequalTo) fails
the run before anything is written when a JSON response says its list is partial, such as GitHub
search'sincomplete_results: true. Both are optional and off by default, so existing syncs
behave as before and neither contract number moves. Seedocs/collection-syncs.md. - API key usage.
GET /api/api-keys/{id}/usageshows when a key was last used, why it was refused in the last 24 hours (scope, content type, expired, revoked), and the content types it names that no longer exist. Refusals are counted in memory, capped per key, and exported asbarakocms_api_key_refusals_totalby reason. - A saved query can group, count and sum in the database.
POST /api/queriestakes optionalgroupBy(a field, with aday,week(ISO week, labelled by its Monday),monthoryearbucket for dates),measures(sum,count,avg,min,max, each with a name, and an optionalviareference field for one hop) andrange(a date field with a relative window such aslast7days, matched without regard to case, orfromandto).POST /api/queries/{slug}/runreturns the groups and totals, withfromandtoin the body to move the range. Figures are computed under the caller's own field and row rules: a field they may not read refuses the run by name, hidden rows are not counted, and days follow the site's time zone. More than 1000 groups, or a query that runs past 30 seconds, is refused with a reason. Preview returns the same rows. A query without the new keys behaves as before. - Saving a query with measures now refuses
fields,sortFieldandlimit,rangeorgroupBywithout a measure, and acountover a field that is not a number.fromandtoat run time need a saved range. A date with no zone is read in the site's time zone, and a malformed stored date is left out instead of failing the run. - An Email step can attach a file an earlier step of the same run produced. Set
Attachmentsto{{actions.N.name}}, where step N is an earlier step that captures an output holding the file id and declares the file inWorkflowActionResult.ProducedFiles(at most 20 are kept, and the attempt says when more were declared). The file may be private, because the run made it. Attachments go out in the order written, and a run file named twice is attached once. An id typed into a field or a parameter is still refused unless it is public and the entry names it, and the existing count and size limits apply. The step inside a Conditional does not get step outputs, and saving a workflow warns about a run file reference there.IFileStoregainsFindForRunAsyncandOpenForRunAsync(default implementations throwNotSupportedException, which the email step reports as a permanent failure),WorkflowContextgainsProducedFilesandOrdinal. - A Request step can keep named values from the response as outputs. A request definition takes optional
outputs(a name and a plain JSON path each, such as$.choices[0].message.content),missingOutput(Fail, the default, orEmpty) andmaxOutputLength(up to 2000). The step returns those values on success, and a later step reads the ones the workflow captures as{{actions.N.name}}. Only the named paths are read, never the whole body or a header. A missing path (underFail), a value over the cap, a response that is not JSON or over 256 KiB, or a value that repeats a credential the request sent fails the step and is not retried. A value that holds{{...}}is kept as that text. - API keys can watch an instance and nothing else. New scope
monitor:readreachesGET /api/contents/countsandGET /api/analytics/**, read only, and gets 403 on every other route. A*key does not imply it, so existing keys gain nothing. Counts also acceptcontent:read. - API keys can read per-check health.
monitor:readnow also reachesGET /api/monitoring/health. A key sees each check's name, status and duration, without description, data or tags. The owner still needs the ViewMonitoring capability, and every other monitoring route stays closed to keys. - A Contacts module: one record for a person or an organization.
BarakoCMS.Contactsstores
contacts with typed custom fields (declared on thecontact-recordcontent type), person to
organization links with a role, and a unique email per tenant. A duplicate email answers 409
naming the existing contact, andPOST /api/contacts/{id}/mergefolds a duplicate into it.
Reads needread_contactsand writesmanage_contacts, both granted to Admin. Published
contacts are served at/api/public/contactswith Public fields only, never email, phone or
address. The module ships its own contract number (HttpContractVersion1) and moves no core
number. SeeBarakoCMS.Contacts/README.md. - A Notifications module.
BarakoCMS.Notificationslets any module tell a user something happened: resolveINotificationServiceand callRaiseAsync. The notification lands in that user's inbox (GET /api/notifications,GET /api/notifications/unread-count,POST /api/notifications/{id}/read,POST /api/notifications/read-all) and is handed to every registeredINotificationSink. A user sees and marks read only their own, per tenant. Email delivery through the host'sIEmailServiceis a built-in sink, off untilModules:Notifications:EmailDelivery:Enabledis true.POST /api/notificationsandGET /api/notifications/adminneed the newmanage_notificationscapability, granted to Admin. The table is created bymigrations/5.2.0/notifications.sql. - A comment thread on any record. The new
BarakoCMS.Commentsmodule addsPOST /api/comments,GET /api/comments?targetType=&targetId=(paged) andDELETE /api/comments/{id}. A comment can be a reply to another on the same record. Reading or adding one needs read access to the record itself, so nobody sees a thread on an entry they cannot read. Content entries work out of the box, and another record type joins by registering anICommentTarget. Comment text is stored and returned as text, never as HTML. Capabilities:read_comments,post_comments,manage_comments. - A Scheduling module.
BarakoCMS.Schedulinglets a module schedule a named action for a due time withIScheduler.ScheduleAsyncand cancel it withCancelAsync. A sweep claims each due entry on one instance, hands it to theIScheduledActionHandlerregistered for its action, and never fires the same entry twice. Recurrence is not in this first slice. - Build triggers. The new
BuildHookworkflow action fires a static build once per burst of publishes. Presets:netlify,vercel,cloudflare-pagesandgithub-dispatch. The first publish in a quiet period (QuietSeconds, default 60) schedules the build, later ones do nothing, and a sweep posts the hook once. The post goes through the webhook path, so a failure shows inWebhookDeliveries.PendingBuildis a new document. Seedocs/build-triggers.md. - A new module, BarakoCMS.CdnPurge, purges Cloudflare by cache tag. Add the
CdnPurgeworkflow action to a workflow that triggers on publish. It purges the entry tag, the type tag and the sitemap tag, built byDeliveryCache.PurgeTagsso they match what delivery reads emitted, each starting with the run's tenant. SetModules:CdnPurge:ZoneIdandModules:CdnPurge:ApiToken. A rejected purge fails the action on the run.DeliveryCache.PurgeTagsis new and public. Fastly is not covered yet. - Delivery reads can be kept in memory, so a repeat does not reach Postgres. Set
Delivery:ResponseCache:Enabledtotrue. It is off by default, so nothing changes until you ask. An entry is keyed on the tenant, the host, the path, the query, theX-Tenantheader andOrigin, and holds only a 200 an anonymous caller was sent. A request with anAuthorization, cookie,X-Viewer-Token,X-Share-Keyorpreviewtoken is never served from or stored in it. A commit evicts the cached reads by the same tagsSurrogate-Keysends (the entry, its type, the sitemap), so a publish or an unpublish shows on the next read from that instance. Bounds:ExpireSeconds(60),SizeLimitMegabytes(64),MaxBodyKilobytes(512),MaxQueryLength(512). The store is per instance, so other instances expire an entry on their own afterExpireSeconds. - Core can reserve against a counter.
IStockReservationstakes units from a counter in one statement that holds the counter's row, so two callers racing for the last unit get one success and one refusal, and the balance never crosses the counter's floor. It runs in the caller's session transaction, so the reservation and a content save commit together or not at all. A hold can expire (ReleaseExpiredAsyncreturns its units), and holds are confirmed or released explicitly. Every receipt, reservation, confirmation, release and expiry is a movement, and the balance is their sum. Not gapless, and notINumberAllocator. New tablesbarako_stock_counters,barako_stock_holdsandbarako_stock_movements(migrations/5.2.0/580-stock-reservations.sql). A background sweep (Stock:HoldSweep:Enabled, on by default, every minute, one instance at a time) returns lapsed holds on counters nobody reserves against again. A reference is at most 256 characters. No HTTP surface yet. - A new module renders an entry to a PDF.
BarakoCMS.Documentsadds theRenderDocumentworkflow action ({ "Type": "RenderDocument", "Parameters": { "Template": "invoice" } }), which draws the entry with SkiaSharp from a narrow template language (titles, headings, paragraphs, rules and table rows with{{field}}holes) and stores the PDF through the Files module. The renderer only draws text, so a template cannot fetch anything. A template naming a field that is not Public is refused, using the same check as outbound requests, and two runs with the same idempotency key store one file. There is no template endpoint yet. - A module can say who it is.
IBarakoModulegains default membersPackageId,PackageVersion,Category(one of Content, Comms, Access, Platform, Insight, Business),DescriptionandGlyph, all null unless declared, andGET /api/modulesreturns them as optional fields. Every first-party module declares them. Existing modules compile unchanged. GET /api/audittakes asincecursor an outside system can poll. Pass a timestamp the first
time and thecursorof each response after that. Entries come back oldest first in bounded pages,
and the cursor resumes exactly where the last page stopped. The same masking and tenant rules as
the list apply. Withoutsincethe list is unchanged. This is the pull half of #689; a push
transport is a later topic on the messaging transport, not a second package.- Test fixture module.
BarakoCMS.TestingshipsFixtureModule(one capability, one content hook, one gated endpoint) andRecordingEmailProvider, so core tests can prove a module mechanism without a shipped domain module. - A form can have an opening and closing date.
PUT /api/forms/{contentType}takes optionalwindowContentType,opensAtFieldandclosesAtField, naming date fields of a singleton content type. A submission outside the window is refused with 403, and editing the date moves the window with no restart. The anonymous form definition reportsopenandclosedMessageonly when a window is set. A form with no window behaves as before. - QR codes for a field's value. New module
BarakoCMS.QrservesGET /api/public/{type}/{id}/qr/{field}?format=png|svg&size=8. A field is encoded only when it is listed underModules:Qr:Fields:<type>(empty by default), the entry is published and public, and the field is Public on its type. Anything else is a 404. Values over 1024 characters are refused, andsizeis capped at 20 pixels per module. - A workflow can publish to an outside service. The new
Publishaction takesUrl,Topic,MessageType,Payloadand an optionalSecret.Payloadis a JSON object of text values, and only what it names is sent: placeholders are limited toid,contentType,status,createdAt,updatedAtanddata.<Field>, and a field that is not Public is refused when the workflow is saved. The action writes anOutboxMessagein the same save that records the run, so the run succeeds once the message is stored and a rolled-back attempt leaves none. A background sender posts it signed like a webhook (X-Barako-Signature, with the message id asX-Barako-DeliveryandIdempotency-Key), retries with backoff from 30 seconds up to an hour, dead-letters afterOutbox:MaxAttempts(10) or a 4xx refusal, and writes each send to the webhook delivery log.OutboxMessageandOutboxStatusare new public models. - Workflows can create an entry and move one through its lifecycle. The
CreateEntryaction creates an entry of a content type fromData.<field>parameters, validated likePOST /api/contents, and returns itsIdas an output. TheTransitionaction runs a named lifecycle transition on the triggering entry as the system actorworkflow. A refused create or move fails the step. Both appear inGET /api/workflows/actions. - A field can be computed. A field definition takes
computed:{ "fn": "hours", "args": ["TimeIn", "TimeOut"], "round": 2 }. The functions areadd,subtract,multiply,divide,round,min,max,hours,days,duration(minutes),concatandcoalesce. The server works the value out inside every write (create, update, transition, workflow) and stores it, so it filters, sorts and delivers like any field. A write that sets a computed field gets a 400 naming it, unless it sends the stored value back. Saving a type refuses an unknown function, a missing input, a type that does not fit, a field that reads itself, a cycle, and a computed field more widely readable than its inputs.POST /api/content-types/{name}/fieldstakescomputed. Rollups, backfilling existing entries andifcome later. - New module BarakoCMS.Reports. Save a report over a content type with one group-by field and count, sum, avg, min and max measures, run it as the caller with a group limit and an optional date range, and export it as CSV with formula characters escaped. POST and GET /api/reports, GET /api/reports/{name}.
BarakoCMS.Payments, a module for taking payment for an entry. A content type is declared payable withPUT /api/payments/payable/{contentType}(amount field, currency, description, the paid and failed transitions).POST /api/public/payments/checkouttakes an entry id and a provider name, reads the amount from the entry on the server, and returns the provider's hosted checkout URL; an amount in the request is ignored.POST /api/payments/webhooks/{provider}verifies the provider's signature, handles each provider event id once, and runs the paid transition as thepayments-webhooksystem actor. Each try is aPaymentAttempt, Sensitive by default. Providers implementIPaymentProvider; none ships yet. Capabilitymanage_payments, granted to Admin.- A workflow can fire on a schedule. Set the trigger event to
Scheduleand give a 5-field cron expression inschedule, read inscheduleTimeZone(tenantfor the site time zone, or an IANA id such asAsia/Manila). Saving returns the next five due times asnextDueTimes. One run is queued per due time however many hosts run, and a host that was stopped over several due times queues one run for the latest. The run reads{{schedule.at}}and{{schedule.previousAt}}. - Optional signed one-click approvals. With
Workflows:ActionLinks:Enabledset to true (off by default,Workflows:ActionLinks:ExpiryHoursdefaults to 72),{{links.action "Approve"}}in an Email action resolves to a signed link for that transition, on that entry, for the user the email is addressed to.GET /api/public/action-links/{token}shows what confirming would do and changes nothing.POST /api/public/action-links/{token}(with optionaldatafor the transition's fields) makes the move as that user, with their permission and row conditions checked at that moment, and spends the link. A link fails once spent, after expiry, when the entry has left the state it was issued in, or when the user may no longer make the move, and every failure answers 404. NewActionLinkUsedocument. - A Booking module. New package
BarakoCMS.Booking(in the Suite). A resource is a document with an IANA time zone, weekly opening hours, date exceptions and a slot length (or free-form). Bookings are ordinary content entries; a lifecycle hook refuses an overlap with another active booking of the same resource inside the write, under a per-resource lock.GET /api/public/availability?resource=&from=&to=returns free slots only, bounded by range and throttled per IP. Admin routes under/api/booking/resourcesneed themanage_booking_resourcescapability. Module HTTP contract version 1. Holds, payment links and .ics files come later. - Sign in with an emailed link.
POST /api/auth/magic-link/requestwith{ "email": "..." }emails a single-use link valid for 15 minutes and always answers 200, whether or not the address is registered.GET /api/auth/magic-link/confirm?token=...only shows a confirm button, so mail scanners cannot spend the link.POST /api/auth/magic-link/confirmwith{ "token": "..." }spends it and returns the same tokens as/api/auth/otp/verify, including the MFA challenge when the account has MFA. A tenant that disallowsotpsign-in disallows links too.MagicLinkTokenis a new document type. - Import a whole spreadsheet on the server.
POST /api/import/spreadsheettakes the file with
the header row, content type and a column mapping, and creates an entry from every data row, not
only the 500 that analyze previews. Same create permission and per-row error report as
POST /api/import/content, one transaction, and adryRunflag. PastImport:MaxRecords,
Import:MaxUploadBytes, the expanded size limit,Import:MaxColumnsorImport:MaxSecondsit refuses or stops and
writes nothing. The new route is additive, so no contract number moves. Seedocs/spreadsheet-import.md.
Analyze now also refuses a sheet with a cell pastImport:MaxColumnsor past Excel's
1,048,576 rows, because the parser pads to the highest address it sees. - A tenant can be moved as a bundle.
GET /api/tenants/{handle}/exportwrites one tenant as a
bundle (the config engine's sections plus its entries, read a page at a time), andPOST /api/tenants/importcreates a new tenant from one. Platform admins only (manage_tenants or
SuperAdmin). Password hashes, API keys and mail credentials are not in a bundle. Members, files,
redirects and the audit log are not carried yet. See docs/tenant-export-import.md (part of #897). - Accounts sit above tenants. A
TenantAccountholds tenants, and every tenant not assigned to one belongs to the implicit default account, so a deployment that creates none behaves as before. A user made a manager of an account (POST /api/accounts/{slug}/managers) lists and edits only that account's tenants through/api/tenants;manage_tenantsstill reaches all of them. An account carries console branding as data: name, logo URL and colours, set withPUT /api/accounts/{slug}and read by the console fromGET /api/accounts/branding, which falls back to the tenant'sBrandingcolours.Tenant.AccountIdis new (null is the default account) and tenant responses and writes carry an optionalaccountId. First slice of #898. Account members and roles beyond the manager grant, and a console that reads the branding, are not in it. - Log events carry the tenant. Once a request has resolved to a tenant, every log line written during it has a
Tenantproperty with the slug, including a tenant set by an API key or station token. A request on the default tenant has none, so single-tenant logs are unchanged. The request log line itself is written before the tenant is resolved and does not carry it. - Core has one inbound webhook receiver primitive.
IInboundWebhookVerifierchecks a provider signature (HmacorSvix), refuses a timestamp more than five minutes from now, and runs the handler once per provider event id. A replay answers 200 and is not processed again; the handled ids areInboundWebhookEventdocuments in the newinbound_webhook_eventstable (migrations/5.2.0/910-inbound-webhook-events.sql).POST /api/webhooks/resendnow uses it, with the same route and the same 401 and 200 answers, so a replayed bounce no longer writes a secondEmailEvent. Connector inbound config and payments adopting it are still open on #910. - A signed link service.
ISignedLinkServiceissues links that name a subject and expire, signed per purpose with a key derived from the signing secret, so a link for one purpose fails for another. A link can be single use or revoked. Preview tokens are now thepreviewpurpose; the older JWT preview token still validates until it expires. - A module can seed, read content and run a lifecycle hook without naming a Marten type.
IBarakoModule.SeedAsync(IModuleSeedContext, CancellationToken),IContentLifecycleHook.BeforeSaveAsync(IContentHookContext, CancellationToken)andIContentReader(get, page, count, read all) are new, andCapabilityDefaults.GrantAsynctakes the seed context. The Marten-typed membersSeedAsync(IDocumentSession, ...),OnBeforeSaveAsync,ContentLifecycleContext.SessionandGrantAsync(IDocumentSession, ...)stay and are marked[Obsolete]with removal in 6.0.0. The new members have defaults that call the older ones, so a module written before this seeds and guards writes as it did. First-party modules override the new member and keep their oldSeedAsync(IDocumentSession, ...)override as an[Obsolete]wrapper, so a host on core 5.0.x that only calls the old member still seeds them. The reader sees the whole database the session points at, with no tenant filter, as a query onContentdid. Four hooks (JournalEntryHook,LedgerService,PageTreeHook, andParentReferenceHookwithParentFields) and the module template's Notes endpoint still use Marten, because they need the write session for locks or number allocation.BarakoCMS.Abstractions5.2.0. - Durable work can run on Wolverine. It runs behind
IDurableOutbox, switched on withJobs:Wolverine:Enabled(off by default, so the Marten job storage stays as it is). A hosted service with no request can queue a job for a named tenant throughDurableTenantPublisher, and its handler runs in that tenant. Adds theWolverineFx.MartenandJasperFx.RuntimeCompilerpackages. Storage migration and the workflow runner port come later (#965). - The branch code lookup is configurable.
Envelope:BranchContentType(defaultBranch) andEnvelope:BranchCodeField(defaultCode) name the content type and field that hold a branch's short code for{branch}series templates. POST /api/contents/batchcreates entries and runs lifecycle transitions in one transaction. Send up to 100 steps, eachcreate(with a client-madeid) ortransition. Every step faces the checks of the single create and the status change: permissions, write sensitivity, the create rule, save and transition rules, validation and hooks. The first refusal answers with the step assteps[<index>]and saves nothing. A transition may name an entry created earlier in the same request. A creator still needsLifecycle:AllowSelfTransition:<Transition>to move on their own entry.Idempotency-Keyapplies as on any write.- A content type can declare a realtime topic. Set
RealtimeTopicon the type and a signed-in reader of the type canSubscribeto it on/hubs/realtimeand readGET /api/changesfor it. A branch scoped type notifies only the readers of the entry's branch. Notices and feed items carry ids and the lifecycle state, never field values.GET /api/contents/{id}, by-slug and the list now returnlifecycleState. - Workflow action
CreateContentcreates a numbered entry. It goes through the same create path asPOST /api/contents, so the type's save rules, validation, lifecycle hooks, numbering and initial state apply, and the entry keeps the run that made it inSourceRunIdand the branch of the triggering entry. A retry of the same attempt reports the entry it already made instead of making a second. It runs as the system, so the caller's Create permission is not asked and fields above Public are dropped. The entry is always a draft, and the user who saves the workflow needs create rights on the type. - Content events carry a branch, a node and an origin sequence. Every content event is stamped with
branch-id(the request'sX-Branch-Id), and, whenBarako:NodeIdis set,origin-node-idand a gaplessorigin-sequenceper node. A content type withBranchScoped: truerefuses a create without a branch, andContent.BranchIdkeeps the branch the entry was written in, rebuilt from the headers.X-Branch-Idmust be one of the ids in the member'sbranchesprofile attribute, else 403. Access rules read the row's branch as$branchand the request's as$CURRENT_BRANCH.ContentTypeDefinitiongainsMergeClassandBranchScoped. Applymigrations/5.1.0/event-headers.sqlbefore deploying (rollback-event-headers.sqlundoes it). The profile attribute namebranchis now reserved for the request's branch: a member write refuses it, and a stored one is ignored, so$CURRENT_USER.branchnow reads the request's branch. Rename a storedbranchattribute before upgrading.BarakoCMS.Abstractionsis 5.1.0. - Lifecycle hooks are told the transition.
ContentLifecycleContext.Transitioncarries the transition name and its from and to states, and is null for a plain save.IContentLifecycleRunnergains an overload that takes it; the existing signature and existing hooks keep working.IContentHookContext.Transitionexposes the same value to a hook that implementsBeforeSaveAsync, and reads null for a context that does not set it. - Workflows a module seeds are module-owned.
WorkflowDefinition.OwnerModuleis set byModuleWorkflows.SeedAsync. Config apply never plans to delete a module-owned workflow and refuses a document that names one with other content, and config export leaves them out. - Modules can add workflow action types.
IBarakoModule.ConfigureWorkflowActions(IModuleWorkflowActions actions)registers anIWorkflowActionunder a type that starts with the module's name and a dot, such asSample.PostItem. A type outside that prefix, one core owns or one another module added stops the host at startup with an error naming the module and the type. The action catalog lists module actions with the metadata of the class the module wrote, and a workflow can be saved with them.BarakoCMS.Abstractionsis 5.1.0. - Entries are numbered per branch when the rule has a series template. A request with
X-Branch-Idnow carries the branch code, read from theCodeof theBranchentry. A create on a type whose template needs a branch is refused with 400 when the branch has no code. - A create keeps a number the client took from a ClientAllocated series. The number must fit the series and not be held by another entry of the type. It is recorded in the external number ledger, so the gaps report counts it. Without the flag the server still replaces any number the client sends.
- The numbering config section and two new fields on the numbering route.
GET /api/config/export?sections=numberingand apply carry each type's rule (includingseriesTemplateandclientAllocated) under the type name. Applying never removes a rule.PUT /api/content-types/{name}/numberingtakesseriesTemplateandclientAllocated. - POST /api/numbering/next hands a client the next number of a ClientAllocated series. The caller needs create rights on the type. The number is committed when it is handed out, and the create keeps it when the entry carries it, so a paper form can be numbered before the server is reachable. A series without the flag, an unknown type, and a branch template with no
X-Branch-Idare refused. - Numbering rules take a series template and a client-allocated flag.
SeriesTemplateputs the branch code in the series name ({branch}.customer), so each branch counts on its own, andClientAllocatedmarks a series a client may take numbers from. A client-allocated series cannot reset. - Members pages resolve for a signed-in viewer.
GET /api/public/pages/resolvehonoursX-Viewer-Token. A valid token for the request's tenant also resolves Members pages, and the answer is private and not stored. A token that does not verify answers 401, as on the core public routes. The response now varies onX-Viewer-Token.IPublicContentProjectorgainsProjectandProjectAsyncoverloads takingmember, andViewerAudienceexposes the token check to modules.
The Pages HTTP contract moves from 2 to 3: a public page requested with an invalid or other-tenant viewer token now answers 401 where it answered 200. - Content types can declare save and transition rules.
ContentTypeDefinition.Rulesis a list ofRuleDefinition. A rule runs on create and update (When: Save) or on a lifecycle transition (When: Transition). Its conditions must hold or the write is refused with 422 and anerrorsentry namedrulescarrying the rule's message. A passing rule can also set fields from templates. Rules can read other entries (related,count,sum,unique,member,covers), compare against$NOWand$TODAY, and call anIRuleLookupa module registers. Seedocs/rules.md.BarakoCMS.AbstractionsgainsRuleDefinition,RuleCondition,RuleWhen,IRuleEngineandIRuleLookup. - Workflow runs can be traced to what they created. An action reports the entries it created with
CreatedRef, kept on its attempt. An entry created by a run carriesContent.SourceRunId.GET /api/contents/{id}/runslists the runs of an entry, and withincludeCreated=truethe runs that created it. A module can fire a trigger with a key (WorkflowRun.TriggerKey) so a retry queues one run.WorkflowDefinition.NotifyRoleOnFailurenames a role to tell about failures. Seedocs/workflow-runs.md. - Four limits are settings.
Workflows:ScheduleGraceDays(default 7) andWorkflows:SchedulePageSize(default 200) set how long after its fire time a schedule workflow can still queue and how many entries one sweep read takes.Workflows:RunsListLimit(default 100) andWorkflows:RunsCandidateLimit(default 1000) bound the runs list of an entry. - Scheduled triggers. A workflow can fire relative to a date field, for example 24 hours before an appointment.
- A
jsontemplate format.{{data.Lines | json}}writes a list, object, string or number as compact JSON, so a workflow can pass a list field to an action parameter that reads JSON text. Text that already holds a JSON array or object is not encoded a second time. Seedocs/approval-by-configuration.md. - Workflow action
Transitionmoves an entry through a named lifecycle transition. It names theContentTypeit moves, and a target of another type, or of another branch than the triggering entry, is refused. The move goes throughIContentTransitioner, so the type's rules, required fields, validation and hooks apply and the audit row records the system actor. It runs as the system, so the transition permission is skipped. An entry already in the transition's end state counts as done. The user who saves the workflow needs the transition right on that type and name. - Run failure handler. A module can implement
IWorkflowRunFailureHandlerto act on a failed or partly failed run. It gets the run and the workflow'snotifyRoleOnFailurerole.
Changed
- BarakoCMS.Testing: the test host pools its database connections (at most 20) and clears the pool when it is disposed, so a long run no longer runs out of local ports.
- A station key can no longer hold a platform capability.
POST /api/stationsanswers 400, naming the capability, when the request listsmanage_tenants,manage_roles,manage_platform_roles,manage_users,manage_email_settingsorview_hidden, the same set member role grants already refuse. Before, a caller who held one could put it on a station key. Station keys already created with one are not touched: list them withGET /api/stations, revoke them and issue new ones. Admin contract moves to 10: a create request that was accepted is now rejected. - Listing entries by
contentTypeignores case. Entries keep the type name as the caller
spelled it, soArticleandarticleentries of one type were two lists.GET /api/contents?contentType=article
now returns both. The stored spelling is unchanged and is documented indocs/content-type-names.md. - A role's permission for a content type applies whatever the case of the entry's
contentType.
Without it, the list found anArticleentry through a rule forarticleand then hid it.
A role that holds two spellings of a type uses the exact one first. - Config apply and export carry a content type's uniqueness rules, and a test now says so. A
new type in a config document is created with its rules, checked the wayPOST /api/content-types
checks them. A document withoutuniquenesskeeps a stored type's rules, and one with different
rules is pointed atPUT /api/content-types/{name}/uniqueness. The behaviour was already there
through the shared upsert; the docs and tests did not cover config. - The private site check is cached per tenant. An anonymous
/api/public/*request in a tenant
that is not in Private mode no longer reads the content table to find out. The answer is cleared
on the node in the same commit that writes, deletes or publishes asiteentry, by any path
(endpoints, config apply, seed files), so a tenant switched to Private refuses the next request.
Another node learns withinDelivery:PrivateSite:CacheSeconds(default 5,0reads every
request). No route, field or status code changes, and neither contract number moves. - CI runs the backend tests as four parallel shards. The required check is still
Backend (.NET);
it now waits for the shards and the script gates, and fails if any of them failed, was cancelled or
was skipped. Classes are balanced by recorded durations (scripts/test-class-durations.json). - Semantic search fails loudly on mixed embedding sizes, and the database image carries pgvector.
GET /api/public/{type}/semanticanswers 409 with a message to reindex when a stored vector and the query vector have different dimensions, where it used to score them 0 and return nothing.Vectors.CosinethrowsVectorDimensionMismatchExceptionfor the same case. The Postgres image in the compose files, the Kubernetes manifest and the test fixture is nowpgvector/pgvector:pg16(Debian based, so an existing Kubernetes volume may need its owner set to uid 999). Nothing uses the extension yet. The AI module'sHttpContractVersionmoves from 1 to 2, since the semantic route can now answer 409 where it answered 200. - Retention sweeps share one primitive. Workflow run retention, webhook delivery retention and the expiry pass in token cleanup now declare what they keep and for how long, and one sweeper does the advisory lock, the tenant walk, the delete and the log. Windows, config keys and per-tenant behaviour are unchanged. Webhook delivery retention now takes an advisory lock too, so two nodes no longer sweep the same rows. Partitioning the run and delivery tables is not part of this change.
- The managed platform guide says how uploads work on Azure.
PostgresFileStorageis durable
across restarts and scale-out, so App Service needs nothing extra for uploads. Azure Blob is not
supported, and the guide names the S3-compatible route (Files.S3withServiceUrl). Docs only,
no route or contract change. - The HealthChecks UI dashboard is gone from the image, and the image runs on
aspnet:10.0-noble-chiseled-extra.AspNetCore.HealthChecks.UI,.UI.Clientand.UI.InMemory.Storageare no longer referenced, which also removes Entity Framework Core and the Roslyn assemblies they pulled in./health,/health/liveand/health/readyanswer as before./health-uiand/health-ui-apiare not served any more. A deployment withHealthChecksUI:Enabledset logs one startup warning naming the setting; to get a dashboard back, hostAspNetCore.HealthChecks.UIin your own application and point it at/health. The final image has no shell, so a container healthcheck that runscurlorshinside it must move to a probe from outside (an HTTP probe in Kubernetes, or the database container's own check in compose). Admin contract is already at 10 for this release. ITokenIssuer,TokenIssueResult,IMfaService,IContentValidatorServiceandIContentLifecycleRunnermoved intoBarakoCMS.Abstractions. Namespaces are unchanged andbarakoCMSforwards each type, so a module can use them without referencing the core.IPermissionResolverstays in the core for now, because its signatures nameReadPredicateandFieldSet. Part of #972.TenantContext,ContentTypeName,ContentTypeFieldLimit,AuditLogandAuditChainmoved intoBarakoCMS.Abstractions. Namespaces are unchanged andbarakoCMSforwards each type, so a module built against 5.0.x loads as before. A module can use them without referencing the core.CapabilityGateandRequiredCapabilitywere already there.BarakoCMS.Abstractions5.2.0.- Behavior change: the branch code lookup (
Envelope:BranchContentType) and the content type of a CreateContent target match the type name ignoring case, as every other lookup by type name does. - Four modules would have been skipped at publish time. BarakoCMS.Abstractions is 5.2.0, BarakoCMS.Email.Smtp is 4.5.0, BarakoCMS.ExternalAuth is 4.8.0 and BarakoCMS.Templates is 5.2.0, so the release publishes the changes each one gained since its last version.
Removed
- The Resend email module moved to the private feed.
BarakoCMS.Email.Resendis no longer built from this repository, and the core and the Suite no longer readResend:ApiKey,Resend:FromorRESEND_API_KEY. This breaks a host that set any of them. Move toEmail:ApiKey,Email:FromorEMAIL_API_KEY, or install the private Resend module from the BarakoCMS.Pro feed and setEmail:ApiKey(or the key in the admin email settings) for it, since it takes its key from the core settings and does not readResend:*. Or switch toBarakoCMS.Email.Smtp. The stored email settings are unchanged. ThePOST /api/webhooks/resendandGET /api/email-eventsroutes belonged to that module and are gone from the Suite.ApiContract.Versionmoves to 11 for the two removed routes; the Settings, Email request and response are unchanged. The settings test send no longer needs an API key, so it works with SMTP and no longer returns 400 for a missing one. A host that still setsResend:ApiKeyorRESEND_API_KEYgets a startup warning when email falls back to the mock (#1216).
Fixed
- Cash flow no longer counts a transfer between cash accounts as a receipt and a disbursement.
ReportingService.CashFlowAsyncnow nets each entry's cash lines and classifies that net, so a
deposit from cash on hand to the bank adds to neither side. Detail lines still show both legs and
closing is unchanged. Accounting 4.5.0. - A workflow schedule armed by a host with a later clock no longer stops the sweep. The entry is
skipped until the sweep clock reaches it, and other due entries still fire. - Two scheduler instances no longer fire one entry twice. The sweep claims a due entry with one
conditional update that wins only when a single row changes. - Validating a schedule-triggered workflow reads its schedule fields.
schedule,scheduleTimeZone
andscheduleCatchUpare now accepted by the validate endpoint, so a bad cron expression or time
zone is reported there instead of passing. - A token refresh no longer uses up the sign-in limit. Refresh has its own limit,
RateLimiting:Refresh(60 per 15 minutes per IP by default), so reloading the console does not lock out sign-in. Login, OTP and MFA keepRateLimiting:Auth, which no longer governs refresh. A refresh with no cookie is still counted.refreshis now a built-in policy name, so a custom policy already namedrefreshfails startup: rename it. - A connector send no longer buffers the whole response. The body is read only as far as the
delivery row keeps it, within 30 seconds of the headers, so a provider that never stops sending or
stops mid-body cannot hold the run. - A sweep that lost its advisory lock could repeat its work, and a held schema lock hung startup.
The scheduled-content and workflow run retention sweeps now check each item again inside the write,
so a second instance that took over finds the transition already done. The schema apply lock polls
with a timeout (5 minutes,Schema:ApplyLockTimeoutSeconds) and fails naming the lock key and the
holder's pid.docs/deploy-in-production.mdsays session locks need a direct or session-pooled
connection. - A sweep that lost its advisory lock kept sweeping. The scheduled-content sweep and the
retention sweeps (workflow runs and webhook deliveries) now ask Postgres whether they still own
their lock before each tenant and stop if not, reporting the lock as not held. The schema apply
lock, the scheduled-content sweep and the retention sweeps share one lease that serialises use of
the lock connection and closes it on any failure. It also looks for a transaction-mode pooler (two
statements on different backends), best effort, since a quiet pooler can hand both to one backend.
A suspected pooler logs an error once per process.Leadership:RefuseBehindPooler(default false,
so nothing changes) makes the sweeps decline and the schema lock refuse to start. - A batch create now sends the realtime notice. The notice looks the entry up after the commit, but a batch commits once at the end, so the lookup ran before the entry was visible and nothing was sent. The notices of a batch now go out after its transaction commits, and none go out for a batch that rolls back.
- A failed batch step names itself and gives its reason. The error for a failing step was
steps[n]as the message with no name, so the rule or hook message was lost. It now has the namesteps[n]and the reason as its message. - A branch scoped type is now enforced on the content routes, not only on the change feed and realtime. A caller who holds branch A could read, list, update, move, schedule and batch-write entries stamped branch B unless an admin had written a
$CURRENT_BRANCHrule. NowGET /api/contents/{id}, by-slug, history, rollback, runs, schedule, update, status change, erase and the batch transition answer 404 for an entry of a branch the caller does not hold. The list and counts leave those entries out, and paging and totals follow. One service,IBranchScopeGuard, holds the rule for the feed and the routes. Types that are not branch scoped are unchanged. See event-envelope.md. - A client-allocated number could be kept under two spellings. Only the form the series formats is kept now (P-0001, not P-00001), so one number has one spelling. The already-issued check reads the same ledger key the number is recorded under, so it also catches a number used twice in one batch or import, and a branch series refuses a write that has no branch.
- Two branches can each issue their own client-allocated number. The "That number is already issued." check looked at every entry of the type, so a second branch with its own
{branch}series was refused P-0001 once the first had taken it. For a series with{branch}the check now looks at the entries of the request branch only. - A content type name is matched ignoring case. Config apply and
POST /api/content-typesstore the name lowercase while a module keeps the case it seeds, sotriggerContentTypes: ["Order"],schedule.contentType, workflow triggers, transitions, listing and the other definition lookups failed on a case difference. Permission lookups prefer the exact spelling and never match a different name. A name matching two stored types that differ only by case, with none exact, is refused (400 on create and list, no grant in permissions). - A workflow CreateContent now numbers a
{branch}series. The action set the branch without its code, so a type with a branch number series refused every entry with "The numbering needs a branch with a code." The code is now looked up the way an HTTP request resolves it. - A signed-out request with
X-Branch-Idwas refused with 403. It is now served, and no branch is stamped on it. No branch is stamped on it. - Scheduled workflow runs get an id made from their trigger key, so two sweeps that see one value at once queue one run.
ContentCreatortakes its rule engine as the last, optional argument again, so code that builds it directly with the earlier arguments keeps compiling.- The
changedrule kind fires on list and object fields. It compared the type name of a list or object, so an edited list read as unchanged or an untouched one as changed. A list or object is now compared as canonical JSON: list order counts, object key order does not, numbers compare as exact decimals. String fields behave as before. - Setting a numbering rule with a
{branch}template answered 500 after saving. The example number is now built with a placeholder branch. BarakoTestHostpools its database connections. Its connection string hadPooling=false, so every query opened a socket, and a run of about 35 hosts left near 13,000 in TIME_WAIT and failed with "Can't assign requested address". It now pools withMaximum Pool Size=20, and clears the pool when the host is disposed, before its database is removed.- A workflow can fill a json field from a template in
TransitionandCreateTask, and a json field only takes a whole list or object.Data.<Field>values such as{{data.BilledLines}}reached a json, object or array field as text, so aTransitionfailed with "expects type 'json' but received 'string'" andCreateTaskstored the text. Now, forTransition,CreateTask,UpdateFieldandCreateContent, a json, object or array field is filled only when the parameter is exactly one{{data.X}}or{{data.X | json}}placeholder whose field holds a list or an object. A template that mixes JSON with a placeholder, such as{"note":"{{data.Name}}"}, or a text field that holds JSON, is refused with a permanent failure that names the field, so a typed value cannot add keys. - A typed dictionary in a content data bag read back as an array. A hook that stored a
Dictionary<string, decimal>(or any string-keyed dictionary other thanDictionary<string, object>) got a list of key and value pairs back. Every string-keyed dictionary is now written as a JSON object, and decimals stay exact. - One save starts a conditioned workflow once. The workflow daemon matched each Created and Updated event against the entry as stored when it read the event, not as that event wrote it. When a workflow's UpdateField was followed by a person's save before the daemon caught up, both events read the person's data, so a workflow conditioned on it was queued twice for one save. Each event is now matched against the data it wrote.
- A workflow applied by config keeps
notifyRoleOnFailure. Config apply dropped it, so a failure handler never saw the role. It is now read on apply, written on export, and a change to it shows in the plan. - A workflow can fill a json field from a template.
UpdateFieldandCreateContentstored the text of{{data.X | json}}as a string in a field that holds a list or an object. For a json, object or array field they now store the parsed list or object, and fail the action when the text is not a JSON array or object. A whole list or object in a placeholder with no format is now its JSON, not its type name.
Security
- A client could keep a number the server never handed out. A client-allocated number is kept only when the next-number route has handed it out, and a numbering refusal on batch, import, site settings or a CreateContent action is a 400 or a permanent failure, not a 500.
- A client-allocated series could use hiLo. It cannot now, because a hiLo counter cannot say which numbers it handed out. Such a rule is refused when it is saved.
- The next-number route resolves the content type (ignoring case, refusing a name two stored types share) before it checks the caller's permission, and checks it on the stored name.
- A saved query returns only the rows of the type it names when two stored types differ only by case.
- A branch code that cannot be part of a series name, or a template name over 64 characters, is a 400 (or a permanent action failure) instead of a 500, and the name is no longer cut short.
- A client number taken by two creates at once is kept once; the other create gets a 409 instead of a 500.
- PUT and DELETE on a workflow a module owns are refused with a 409, as config apply already does.
- The runs list of an entry (
includeCreated=true) shows a run of another entry only through the actions that name this entry, without their error text, and lists a created entry only when the caller may read it. - A CreateContent action reports an existing entry under its id as done only when it has the same type and branch and was made by the same run. Otherwise it fails for good.
- The Transition action could move entries of another branch. It now moves only entries of the trigger entry's own branch. A trigger entry with no branch no longer moves entries that have one.