Changelog
Changes that matter to an app built on ddcore, newest first. The format follows Keep a Changelog; versions follow the policy in conventions. Anything under Breaking carries the upgrade path, and apps should keep their ddcore: range below that release until they have taken it.
This page holds Unreleased and the current minor series; each older series has its own page in the sidebar. The binary serves the same text: ddcore://changelog is this page, ddcore://changelog/<minor> an older series, and the whats_new MCP tool reads across all of them.
Unreleased
0.27.7 — 2026-10-06
Added
ddcore exec --args -reads the JSON arguments from stdin, and--args-file <path>from a file (-is stdin too). A payload carrying personal data no longer has to sit in argv, wherepsand/proc/<pid>/cmdlineshow it to every user of the host, and a large batch is no longer capped byARG_MAX. Malformed arguments are now reported before the database is opened (#103).
Fixed
ddcore migrate --pruneplans its drops after theafterSchemapatches instead of before them. A release that backfills and contracts in one go — both patchesafterSchema, the contract dropping the old column itself — no longer fails, either on a refusal for data the backfill was about to copy or on aDROP COLUMNfor a column the patch had already dropped. An orphan a patch writes into is now refused rather than dropped with what the patch wrote.migrationsdocuments the contract-in-the-same-release pattern and why the contraction is notbeforeSchema(#102).
0.27.6 — 2026-10-06
Changed
limit=0onGET /api/resource/<DocType>— and soddcore.db.getList(doctype, { limit: 0 })in a desk script — returns every matching row, aslimit: 0does on the server; it used to be read as "left out" and return a page of 20, so a bulk action over "all" silently acted on the first page. A negative or non-numericlimitis now aValidationErrorinstead of returning everything or 20.form-apidocuments the deskgetList's default of 20 (#101).
0.27.5 — 2026-10-06
Added
ddcore.session({ user, fullName, roles, lang }) andddcore.hasRole(role)in desk scripts, read from the desk's boot, so a form or list script can show a button only to the roles the server will accept. They hide UI and grant nothing: the method still checks (#93).refDoctypeon a workspace number card or chart with amethod(): the endpoint answers 403 to a user without read on that DocType before running the method. Amethod()runs as the caller with no DocType check of its own, andreport-apinow says so, along with what the workspacerolesand adoctypecard enforce (#94).defineListView({ toolbarActions }): buttons on the list toolbar that run with no rows selected, in every view.onClick(list)getsdoctype, the view'sfiltersandrefresh(); an optionalcondition()decides whether the button shows (#96).
0.27.4 — 2026-10-06
Added
ddcore.errorLog.record(error, { method?, context? })writes anError Logrow on a transaction of its own and returns its id, so code that catches a failure can leave it where operators andddcore doctorlook and go on: the row stays whether the caller's work commits or rolls back. It carries the request's id, orjob:<id>in a job's body, lands in the current tenant, and never throws. The pattern for a job of independent steps — each in a savepoint, a failing one recorded rather than rethrown — is under "Savepoints" incontroller-api(#92).ddcore.job.current()returns the running job'sJobInfoin its body and callbacks (nulloutside a job), andJobInfohasstarts: how many times a worker began the job, a run given back by a shutdown included.attemptstays the same across a give-back, sostarts > 1is how a body doing non-idempotent work learns that an earlier run may already have done it. The admin job listing,GET /api/jobsandddcore jobs showshowstartstoo. The ops docs now say thatmaxAttempts: 1is not "runs at most once" (#91).shutdownGraceSecondsinddcore.json, orDDCORE_SHUTDOWN_GRACE_SECONDS(default 30): how long a process told to stop lets its running jobs finish before it gives them back to the queue. Set the platform's stop timeout above it (Composestop_grace_period, KubernetesterminationGracePeriodSeconds) (#99).ddcore tenant adopt <slug> --dry-runprints, table by table, the rows the adopt would move into the tenant and every collision with rows the tenant already has, on the primary key and on each unique index (up to ten keys each), moves nothing, and exits non-zero on a collision. An app no longer has to re-derive the adopt's rule in SQL to fail before the cutover (#100).ddcore import run|status|reconcile <dir> --tenant <slug>loads an export into a tenant: the documents, the import ledger, the numbering series and theimport.runaudit events land in the tenant, and nothing else on the site moves.--tenantbeforeimportmeans the same. The load leaves out shared DocTypes and theAdminandGuestaccounts, and refuses an attachment whose url a file of another space holds. The same export can be loaded into two tenants. A run records its tenant:--resumerefuses another one, andstatuslists the runs of one space. The MCPimporttool takestenant(#100).ddcore.db.tryLock(key): the lock ofddcore.db.lockwithout the wait. It returnstruewith the key held to the end of the transaction, orfalseat once while another transaction holds it, so a job can queue itself again and free its worker instead of parking it for as long as the holder takes. Scoped to the tenant, aslockis (#98).ddcore.enqueue(..., { runAfterSeconds }): the job's delay from now, in seconds (#98).- Worker pools per queue:
"workers": { "default": 2, "bot": 2 }inddcore.jsongives a queue workers of its own. A named pool serves its queue only;default, which the object must name, serves the default queue and every queue not named, the scheduler's included. A number still means that many workers for every queue.ddcore jobs work --queue bot[,other] [--workers N]starts only those pools, so a queue can have a process of its own;DDCORE_WORKERSoverrides the setting with a number;ddcore doctorprints each pool (workerPoolsin--json) (#98). ddcore.throw(msg, opts)takesstatus, an HTTP error status (400-599) that overrides the type's, andretryAfter, seconds stored asextra.retryAfterthat also set theRetry-Afterheader. The SDK typesextra(it reaches the JSON error body aserror.extra) and exportsErrorType, the known types, andThrowOpts.controller-apihas the table of types and statuses, and the webhook examples refuse with{ type: "PermissionError" }(403) instead of a bareddcore.throw, which is a 417 (#89).
Changed
- A job's
timeout, an administrative cancel and a worker shutting down now also cut the outbound call the job is waiting on —ddcore.http.*,ddcore.files.save({ fromUrl }),ddcore.push.send, an external database query, mail and webhook delivery — instead of waiting for it to return. A job'stimeoutis now a bound on its HTTP calls too. Calls made while serving a request are unchanged (#99). ddcore tenant adoptrefuses with the list of colliding keys — table, columns and values — instead of stopping on the first raw primary-key violation. The docs now say what else an adopt moves: the platform space's Singles, vault secrets, Error Log, Audit Event, Version and Feedback rows (#100).ddcore importno longer refuses--tenantbefore the command. The import ledger (ddcore_import_record) is per tenant on a site with tenancy, andddcore tenant adoptmoves the platform space's ledger with everything else, so an export loaded before the adopt is not loaded again into the tenant after it.migratere-keys the ledger; nothing to do (#100).ddcore.enqueuerefuses arunAfterit cannot read with aValidationError, and one given together withrunAfterSeconds. It used to run such a job at once, which turned a job meant to wait into a hot loop. An ISO timestamp andYYYY-MM-DD HH:MM:SSare read as before, and a date (YYYY-MM-DD) or a timestamp without a zone is now read in the site's timezone too (#98).
Fixed
- SIGTERM (or Ctrl-C) no longer drops the jobs a process is running.
ddcore start,ddcore devandddcore jobs workstop claiming jobs, let the running ones finish withinshutdownGraceSeconds, give the rest back to the queue with their attempt returned and noonFailure, and only then exit. They used to exit at once: a job blocked inddcore.httpstayedrunninguntil its lease expired and was then failed, its attempt spent, as the docs said it would not be (#99). - An error thrown from app code answers its type's status for every type the server knows:
TooManyRequestsError(429),UnavailableErrorandMaintenanceError(503) andMethodNotAllowedError(405) used to answer 500 fromddcore.throw. A Go error crossing app code — theMaintenanceErrorof a write made while the site is paused, say — keeps its status and itsRetry-Afterinstead of becoming a 500 with no header. An unknown type is still a 500 (#89). - A missing mail template, print template or notification rule raises
DoesNotExistError(404) instead of the unknownNotFoundError, which answered 500 (#89).
0.27.3 — 2026-10-06
Added
ddcore.httptakesmaxRedirects(default 10):0returns the 3xx with itsLocation, so an app can follow a redirect itself and choose the headers it sends (#97).
Fixed
Date.parseandnew Datein server code read a date-time written with a space instead of theT, as Postgres writes it:"2026-10-05 10:00:00-04:00"lost its time of day and"2026-10-05 22:31:52.767353+00"gaveNaN. Both now give the instant Node gives (#90).ddcore.log.*andconsole.*no longer log an object as"[object Object]": a plain object's keys become fields of the structured record (ddcore.log.warn("retry", { status })logsmsg="retry" status=…), and other values join the message as JSON. A value with a cycle no longer makesconsole.logthrow.ddcore evalprints the fields after the message (#88).ddcore.httpandddcore.files.save({ fromUrl })no longer send the caller's headers to another host on a redirect. A hop to another host, or from https to http, now drops every header the call passed exceptContent-Type; net/http only droppedAuthorizationandCookie, so a token sent asapi_access_tokenorX-Api-Keyreached the redirect's target (#97).- The access log, the panic log and the Error Log title no longer print what follows a method's name in
/api/method/<path>/<tail>: they show/api/method/<path>/…. A webhook that carries a token in itspathTailwrote it to the logs on every delivery (#95). ddcore <command> -hand--helpprint the command's options (ddcore test -hlists--filter,--appand-v) or, for a command with subcommands such asimport,jobsoruser, its usage. Both failed with "unknown flag" and pointed at themselves (#87).
0.27.2 — 2026-10-05
Added
defineReport({ description })draws a line under the report's title, translated likelabel: a place to say what the report shows and how to read it (#81).ddcore.httpsends binary and multipart bodies withbodyEncoding:"base64"sends a base64 stringbodyas raw bytes, and"multipart"sends an array of parts ({ name, value }or{ name, base64, filename?, contentType? }) asmultipart/form-data, boundary included. AContent-Typeheader is now matched without regard to case. The typeHttpMultipartPartis exported (#83).ddcore.webhooks.verify(secret, headers, rawBody, { toleranceSeconds = 300 })checks a Standard Webhooks request on an inbound method in one call: it decodes awhsec_<base64>secret, accepts severalv1,…signatures, in one header or repeated ones, compares in constant time, enforces the timestamp window (toleranceSeconds: Infinityswitches it off), reads header names in any case and returnsfalserather than throwing when a header or the secret is missing.ddcore.crypto.hmacSha256takes{ keyEncoding: "utf8" | "base64" | "hex", output: "hex" | "base64" | "base64url" }; without options it answers as before (#84).ddcore.crypto.randomToken(bytes = 32)(base64url fromcrypto/rand),randomInt(min, max)(uniform in[min, max)) andsha256(data, { output })(hex by default), for session tokens, one-time codes and the hash to store instead of a token (#85).ddcore.push.send(subscription, payload, { ttl, urgency, topic })delivers a Web Push message from server code: the payload encrypted for the browser's subscription (RFC 8291,aes128gcm, at most 3993 bytes) and the request signed with the site's VAPID key (RFC 8292), read fromDDCORE_SECRET_VAPID_PUBLIC_KEY,_PRIVATE_KEYand_SUBJECT. It returns{ status, body, headers }whatever the status, so the app deletes a subscription that answered 404 or 410. An endpoint must behttps:outside development.ddcore.push.publicKey()gives the page the key it subscribes with, ornull, andddcore push keysprints a new pair, refusing a--subjectthat is not amailto:orhttps:URL. Seepush(#82).
Fixed
ddcore.utils.randomStringusedMath.random, which is not safe for a credential. It now draws fromcrypto/randwithout modulo bias, with the samea-z0-9alphabet and the same handling ofn: a fraction rounds up and anything not positive gives"". It now throws above 65536 characters. The ids ddcore generates for new documents share that bias fix (#85).
0.27.1 — 2026-10-05
Added
- The desk draws
keyandbug. - The desk draws 201 more lucide icons, 300 in all: money and commerce (
coins,piggy-bank,hand-coins,shopping-cart,store,package,warehouse,truck,barcode, ...), people (user-pen,user-check,id-card,graduation-cap, ...), messages (phone,message-circle,messages-square,video,megaphone, ...), documents (files,clipboard-list,file-spreadsheet,signature, ...), time, places (map,globe,church,hospital, ...), status, actions, systems (network,database,server, ...) and more. lucide's old namescheck-circle,x-circle,alert-circle,help-circle,circle-help,unlock,pie-chart,line-chart,smileandfingerprintwork too. The full list is under "Icons" inreport-api(#80). allowCreate: falseon a DocType says only server code creates its documents, such as a Transfer made by a button on another form. The desk then offers no way to make one, to Admin too:/api/metareportscreateandamendas false, so the list has no New, a Link no+, the form no Duplicate or Amend, and Data Import no insert mode. A typed/newshows a notice.POST /api/resource/<doctype>and a spreadsheet insert are refused. Server code inserts as before. An extension may set it on another app's DocType. See "DocType properties" infieldtypes(#79).- The list shows the DocType's
descriptionunder its title, translated likelabel: a place to say where the documents come from (#79).
Fixed
- API Key, Error Log and Version show an icon in the desk's System menu (
key,bug,history) instead of the dot an item without one gets. /newfor a DocType the reader cannot create shows a notice with a link back to the list. Before, it opened a form that took the typing and whose save the server refused (#79).
0.27.0 — 2026-10-05
Breaking
- An
iconthe desk does not draw now fails the load, on a DocType, a workspace or anything a workspace lists (sidebar,shortcuts,links), naming where it is:workspace "Payments", sidebar[3]: icon "piggy-bank" is not one the desk draws. Before, the desk drew a circle and nothing said so. Upgrade: runvalidate_meta(or start the server) and replace each name it reports with one from the list under "Icons" inreport-api(#78).
Added
- The desk draws
wallet,credit-card,banknote,landmark,percent,qr-code,send,inbox,undo-2,book-open,briefcase,building,wrench,file-text,folder-tree,chart-bar,penanduser-plus, and accepts lucide's other names for icons it has:triangle-alert,chart-column,ellipsis,square-check-big,edit-2. The list inreport-apiis generated from the desk's own table, so it names every icon there is (#78). - Feedback. Every desk user can report a bug, an improvement or a feature request from Feedback in the user menu. The dialog asks for the fields of the chosen type, takes up to 10 files (picked, dropped or pasted screenshots) and an audio note recorded in the browser, and sends the current page's address and context data (both checked by default; the context is previewed before sending). It is written as a core
Feedbackdocument, in the platform space on a site with tenancy, stamped with itssource_tenant. The platform's System Managers get it in the desk inbox, and the addresses in"feedback": {"to": [...]}(orDDCORE_FEEDBACK_TO) get a mail with the files. The author follows status and response under "My feedback". It is on by default;"feedback": {"enabled": false}orDDCORE_FEEDBACK=0turns it off. App scripts can open it withddcore.ui.openFeedback({ type, title }). Endpoints:POST /api/feedback,GET /api/feedback/mine. Seefeedback. - Audio files (
.webm,.ogg,.m4a,.mp3,.wav, …) are served in place with their audio type, like images and PDFs, so an<audio>element plays them. - The desk draws
bug,lightbulb,mic,squareandtrending-up.
Changed
- The sidebar's Notifications and To-Do links moved from the top of the menu to its footer, just above the user's avatar, so they no longer read as one of the workspace's modules. Their counters and the collapsed rail work as before.
Fixed
- The desk's own icons that drew a circle — the open workspace menu's chevron, the assign dialog's and the editable title's — draw what they name; in the browser, an unknown name draws a circle with a console warning (#78).