What's new
Changelog
Every Deckyard release, in plain language. The commit-level detail lives on GitHub; here is what it means for the people using it.
-
v1.97.5 Latest
A crafted video source could run a script in the app. The Bunny embed is now rebuilt from its ids, and anything else embeds nothing.
Security fix. The video slide used a pasted Bunny URL as its frame address after only checking that the text mentioned Bunny’s player host. Anyone who could write to a slide, including a collaborator, an import or an agent, could store a
javascript:address that ran in the page for everyone who opened the deck in the app. The same check also let a foreign page be framed. Published editions were not affected. If you run your own installation, upgrade to this release.The fix changes how a Bunny URL is read. The video slide now takes the library id and video id from a
playorembedURL on Bunny’s player host and rebuilds the embed address from those two. A query or fragment on the pasted URL is dropped. A token-authenticated embed URL therefore no longer carries its token, so if you use Bunny’s embed token authentication, check those videos after upgrading. A URL that is anything else embeds nothing, and the slide shows its empty state.This release adds no database migration or configuration requirement.
-
v1.97.4
Bulk export now applies the existing SSRF guard to remote images. Leaving a view also stops its running task, and AI decks write mixed openings as prose.
Security fix. A signed-in user could make the bulk export fetch a URL from the image library or a theme logo, including an address on the server’s internal network, and receive the response in the ZIP. Bulk export now uses the same guard as the other export paths. If you run your own installation, upgrade to this release.
The guard changes what ends up in a bulk export. Internal addresses, redirects and responses that are not images are no longer packed, so a logo or library image on an intranet host loses its bytes in the ZIP. Downloads now stop after 8 seconds or 15 MiB. Each refused image counts as a failed download in the existing export warning. Public images and files served by the installation itself are still included. If you rely on internal image hosts, check that warning after your next bulk export.
Long tasks now stop when you leave their view. Navigating away during an analysis, import or export closes its progress window and prevents a late redirect or download. Cancelling does not undo work the server has already done.
When AI drafts a deck, an opening that mixes goal, status, roles and scope now becomes one or two connected sentences. Lists whose items really are of one kind stay lists.
This release adds no database migration or configuration requirement.
-
v1.97.3
The export-dialog screenshot recipe now includes the whole dialog, including the slide selection and every export option.
The capture recipe for the full export dialog now uses a taller viewport. It includes the slide selection and every export row without clipping the dialog or hiding content. The check that refuses a truncated capture remains in place.
This changes the screenshot recipe; the export dialog in the app is unchanged. This release also updates bundled dependencies and adds no database migration or configuration requirement.
-
v1.97.2
Adding slides to an existing deck with AI now follows the same image rule as creating one: images are copied into your media library, and anything that cannot be copied is reported.
Adding slides to an existing deck with AI no longer leaves image links that point elsewhere. In the editor, MCP
append_slidesand the publicappend-slidesAPI, image URLs in your source text are now candidates the generation can choose from, and each chosen image is copied into your media library. This closes the gap that 1.97.1 left open.What API and MCP callers notice
The rules match deck creation. Copying only happens when uploads are on and the caller may upload, so an API key needs
writenext toai; a key with onlyaicauses no fetch.An image that cannot be copied leaves an empty field. So does any image URL the model writes itself. MCP
append_slidesand the publicappend-slidesAPI now returnwarnings, whereindexcounts the new slides. In the editor, the warning appears in the review grid above the new slides.Only declared image fields are checked. Markdown images in body text are not.
This release adds no database migration or configuration.
-
v1.97.1
Images chosen from source text are copied into your media library before an AI-generated deck is saved, and card headers stay readable in static exports.
An AI-generated deck no longer stores image links that point elsewhere. When the source text for
create_presentation, the public AI wizard API or an editor wizard contains image URLs, the generation can choose from them and each chosen image is copied into your media library before the deck is saved.What API and MCP callers notice
Copying only happens when uploads are on and the caller may upload. An API key also needs the
writepermission next toai; a key with onlyaicauses no fetch.An image that cannot be copied leaves an empty field, and the deck is still saved. So does any image URL the model writes itself. MCP
create_presentationand the public wizard API now report these inwarnings, each with the slide’s position and a message. Adding slides to an existing deck is not covered yet.Readable card headers in exports
Card headers and subheadings on the icon card grid no longer turn white on light backgrounds in PNG, PDF and MCP previews, and in PPTX where the slide is rasterized. The live editor and presenter are unchanged.
Forks
If your fork keeps the gradient in PDF exports, replace the shared
STATIC_GRADIENT_CSSgate as a whole. Keeping the gradient while clearing its text colours selects the wrong pair.This release adds no database migration or configuration.
-
v1.97.0
Sandbox guest cookies are now signed and hidden from page scripts. Sandbox operators must configure a secret before upgrading.
The public sandbox now signs each guest session and keeps its cookie inaccessible to page scripts. Sandbox guests cannot create API keys, and sandbox mode refuses to start with collaboration enabled.
Before upgrading a sandbox
Set a random
AUTH_SECRETof at least 32 characters before upgrading and disableCOLLAB_ENABLED. For Docker Compose, put the secret in the.envfile next to the compose files and restrict it to mode 600. You can generate a secret withopenssl rand -hex 32.Existing unsigned guest cookies become invalid. Visitors start as a new guest, and decks belonging to the old guest session become inaccessible. Changing the secret later also starts every guest over.
Publishing fix
Sealed editions no longer include the contrast check’s source URL bookkeeping. The source deck and the rendered contrast verdict stay the same.
This release adds no database migration. Installations upgrading from 1.95.0 also receive migration 107 from 1.96.0 for library-item comments.
-
v1.96.0
Discuss shared library slides in a comment thread, and unlink another maker’s slide before editing it. Publishing also handles disk uploads and S3 filenames containing spaces.
Slides linked to another maker’s library item are now locked in the editor. Choose Unlink and edit to make your own copy before changing its content. The original library item stays with its maker.
Shared library items now have their own comment thread, so feedback stays with the reusable slide.
Publishing fixes
Publishing now reads root-relative
/uploads/files from disk first, falling back to the configured S3 bucket only when the file is missing. S3 upload filenames containing spaces can also be sealed into an edition. The configured public base and upload-key restrictions still apply.For self-hosters
Migration 107 adds storage for library-item comments. No new configuration is required. If you use the renderer runner, register this release’s renderer before publishing new editions, following
docs/ops/renderer-runner.md. Installations converting old publications should repeat their dry run on this tag before applying the conversion; these fixes alone do not prove that every source deck can publish. -
v1.95.0
A slide copied from the slide library now shows when its item has a newer version, with View, Update and Ignore. Installations with S3 media can publish editions with their own uploads, and edition visits count in deck analytics again.
A slide you insert from the slide library now remembers which item and version it came from. When the item’s maker saves a newer version, the deck owner gets a notification, the slide’s thumbnail shows a marker and a bar over the canvas offers View, Update and Ignore. View shows your slide and the newest version side by side. Update takes the new content in every language the item has and keeps your speaker notes; a deck language the item lacks keeps its text and is marked as changed in the translation view. Ignore skips that version until a later one appears. Slides inserted before this release have no link.
Publishing with S3 media
On installations that store uploads in S3, publishing refused decks with their own uploaded images in 1.94.0. Editions now read public upload objects through the configured S3 provider and seal them into the edition, so the published version no longer depends on the original object. Only the configured public base address and public
uploads/keys are accepted; no other URL in a deck is fetched. ImageKit, Bunny video and database slide types still need an explicit conversion before such decks can publish; see the GitHub release anddocs/reference/publication-editions.mdfor the source policy.Analytics
Visits to a published edition (canvas, reader and embed) count in the deck’s analytics again, as anonymous visits per version.
For self-hosters
Migration 106 adds the
slide_library_linkstable; it is additive and has no backfill. Using S3 storage, check thatS3_PUBLIC_URL(or the derived bucket address) matches the base address your decks reference, then runnode scripts/scan-edition-input-size.jsbefore publishing: it now measures S3 objects too. The unused standalone embed player SDK has been removed; edition embeds keep working. If you useRENDERER_RUNNER_SOCKET, register this release’s renderer as described indocs/ops/renderer-runner.md. -
v1.94.0
Publishing creates a fixed edition with its own link. Later edits stay in your working deck; a new publication gets a new version. Self-hosters must convert existing links and update publish API calls.
Publishing now captures a fixed edition: its slides, theme, images and renderer belong to that version. Editing the source deck leaves the published edition unchanged. Publishing again creates a new version with its own link, canvas, reader and downloadable verification package. Unpublishing withdraws every edition in the series; their addresses return a 410 response.
Editing together
The Fields form and canvas selection follow the item you were editing when a collaborator moves it. A remote change keeps the correct title for your language, including the edit pencil. Writing one part of a language version no longer erases its other stored fields.
Before upgrading
This release changes the publishing API and includes database migrations 103-105. Take and verify a restorable database backup first. The migrations add edition and revocation storage, add legacy-link mappings, remove the old publication descriptor from decks and remove the obsolete preview setting. Rolling the schema back does not restore that data.
Existing publication and embed links return 404 until you run
node scripts/migrate-publications-to-editions.js. Start without--apply, preserve the report and review every refusal, including trashed sources. After review, run with--applyand test the old links: they redirect to the first edition, even after you publish later versions. Then check obsolete previews withnode scripts/sweep-published-previews.js --checkbefore using--apply.Publish API calls now require
expectedRevisionandidempotencyKey; a new edition returns 201, a repeated request returns 200. Slug editing and preview regeneration endpoints are removed. Edition visits currently do not appear in visitor analytics, and the old embed SDK controls are unavailable on edition canvases.If you use
RENDERER_RUNNER_SOCKET, register this release’s renderer and check thatrenderer-runneris green in the doctor before publishing. Keep older renderer registrations for existing editions. Seedocs/ops/renderer-runner.mdand the 1.94.0 release on GitHub for the complete upgrade instructions. -
v1.93.1
With live edits on, the Fields form follows a collaborator who reorders items, so your typing lands in the card you see; every dialog fits a phone screen, and several languages read more naturally.
With live edits on, when a collaborator moves an item in a list (a card, a timeline entry) while you have the Fields form open, your form now catches up with the new order as soon as you leave the field. Until then, whatever you type goes into the item the row shows, not into whichever item now sits at that position. Before this release, an edit there could overwrite another item’s text in another language.
Phones
Every dialog now stays within the screen on a phone: no more buttons or fields cut off at the edge.
Languages
Dutch action labels use the infinitive throughout, Danish uses one word for duplicate, Portuguese validation messages are direct instead of opening with “Por favor”, and retry messages in German, Spanish and Portuguese follow the register of their language. Terms around content fields are consistent across Dutch, Swedish, Polish and the other interface languages.
For self-hosters
There is no migration and no new setting. If you use the renderer runner (
RENDERER_RUNNER_SOCKET), register the renderer of this release as described indocs/ops/renderer-runner.md.See the 1.93.1 release on GitHub for the complete changes.
-
v1.93.0
Export one slide or a selection as PDF or as a ZIP of PNGs, tables and live edits keep every language in step, and failed requests explain themselves in your interface language.
The export dialog now asks which slides you want: all of them, the current slide, or a selection such as
1,3-5, prefilled with the slides you selected in the slide list. The choice applies to the slide PDF and the ZIP of PNGs; every other format still exports the whole deck. The file name says which part it holds, and a selection that cannot be exported is explained right under the choice.Translated slides
Deleting a row or column in a table now does the same in every language version, and edits made by collaborators during a live session reach each language per item instead of overwriting the whole list. After you edit in the Fields view, the Inspector follows along; after moving an item with the keyboard, focus stays on the item you moved.
Messages in your language
When a request fails without an explanation from the server, the password forms and some thirty other forms now show their own translated sentence instead of an English transport error. A shared link that is refused names the language version that is active. Polish counts of two to four use their own grammatical case (“3 linki”).
Fixes
A live-only slide such as the follow-along invite no longer shows an Accessibility section it cannot use, and the export warning about incomplete slides recognizes those slides too. The comment card on a spot in a slide posts through the comments panel, so there is one place where a comment is written.
For self-hosters
There is no migration and no new setting. The slide PDF and PNG ZIP export routes accept
?slides=1,3-5; an unreadable selection, or one on another format, is a400 invalidnaming theslidesfield. If you use the renderer runner (RENDERER_RUNNER_SOCKET), register the renderer of this release as described indocs/ops/renderer-runner.md.See the 1.93.0 release on GitHub for the complete changes.
-
v1.92.1
Dates, counts and labels follow your interface language, the share dialog stays whole on a phone and at 200 % zoom, and removing or reordering items in a translated slide now reaches every language.
Dates, times, relative times (“3 days ago”) and counts now follow the language of the interface, and labels use one word per field, status and setting across all languages. The interface says organization everywhere instead of mixing in workspace, and the Polish share dialog writes its diacritics.
Narrow screens and zoom
At 320 px wide, or at 200 % zoom, the share dialog keeps its visibility options, the invite row and the list of collaborators inside the Team panel; a row that no longer fits wraps instead of spilling out. The actions in the notifications panel and the ownership filters above your presentations wrap the same way.
Translated slides
Removing, moving or swapping an item in a list (timeline entries, cards, an image set) in one language version now does the same in every other version, so the versions no longer drift apart. Filling in missing translations translates each gap on its own instead of borrowing text from neighbouring items.
Fixes
A gallery made by an import, the AI or an agent without any images no longer gets demo photos. A comment opened from the lightbox is laid out like the thread in the comments panel. Writing a field a slide type does not declare is refused with a clear
400 unknown_fieldin the editor and over MCP, as the public API already did.For self-hosters
There is no migration and no new setting. If you use the renderer runner (
RENDERER_RUNNER_SOCKET), install it again from this release: its supervisor now starts at boot, so after a host reboot rendering comes back without an operator step. Then register the renderer of this release as described indocs/ops/renderer-runner.md.See the 1.92.1 release on GitHub for the complete changes.
-
v1.92.0
The AI deck review shows why each slide looks the way it does, quotes your source where it can and suggests where a picture belongs. Agents can leave a note per slide and hand you a review link. ImageKit picks save again.
After the AI builds a deck from your text, file or Notion page, the review now lays out every slide next to its explanation. Where the explanation rests on your source, it quotes the passage it was checked against; once you change a slide, the explanation says it describes the earlier version. You can reopen the review later from the editor.
Pictures you choose, not stock photos
The AI no longer fills an image with a stock photo. A slide that needs a picture keeps an empty image field, the review offers Choose image on it, and suggestions between slides let you add an image slot or ignore the idea. Up to three alternative slide types stay listed after you switch.
A slide whose essential picture is still empty counts as unfinished. The editor keeps it, but presenting, every export, publishing and every reader channel leave it out; the editor names the skipped slides before you present or export, and publishing refuses with
422 incomplete_slides. PPTX, the handoff ZIP and print now also leave out draft and hidden-from-export slides, as the other formats already did.A note per slide from agents
MCP tools (
create_presentation_from_slides,add_slide) and the public API (POSTandPUT /presentations,POST /presentations/:id/slides) accept areviewNoteper slide, up to 500 characters. It is shown in the review, never in the deck itself, and only to people who may edit the deck. Answers carry areviewUrlthat opens the review; that link never offers to discard the deck. The public API refuses raw_ai*keys with400; aPUTwithoutreviewNotekeeps the stored explanation.Fixes
Picking an image from ImageKit no longer makes an image slide impossible to save; migration 102 keeps
imagekitFileIdon image and image-text slides instead of stripping it. Four core themes gain title background presets. Labels for sharing, roles, search, library actions, progress and errors are more consistent across languages.For self-hosters
There is no new migration. A new optional setting,
RENDERER_RUNNER_SOCKET, connects frozen-edition rendering to the Linux renderer runner; without it those renders refuse, as before. The Docker image gains a stage for the renderer bundle, andnode scripts/doctor.jsreports the runner. Setup is indocs/ops/renderer-runner.md.See the 1.92.0 release on GitHub for the complete changes.
-
v1.91.1
Existing content-quality issues no longer block unrelated edits or switch your editing language. Explicit repair and a copyable error report keep recovery in your hands.
An existing quality issue, such as an invalid date in another language, no longer blocks unrelated authoring edits or automatically takes you to that language. Your existing main content is preserved, while structural metadata is normalized where needed.
When content needs repair, the editor offers an explicit recovery action and an error report you can copy, including a fallback if clipboard access is unavailable. The report retains the relevant error and editing context.
AI authoring tools also expose optional accessibility overrides. There are no new configuration settings, dependencies or database migrations in this patch. If you are upgrading from 1.90.0 or earlier, read the 1.91.0 upgrade notes and back up before migration 102.
See the 1.91.1 release on GitHub for the complete changes.
-
v1.91.0
Slide writes now reject unknown fields consistently. Back up before upgrading: migration 102 removes unused stored keys. Translation labels and narrow-screen layouts are clearer.
The editor, public API and MCP tools now agree on which content fields a slide type declares. An undeclared top-level content key is refused with
unknown_field; the public content schemas expose the same restriction throughpropertyNames. Integrations must remove unsupported keys before adding or updating a slide.Back up before upgrading
Migration 102 removes undeclared keys from stored decks and their language versions, saved versions, comment snapshots and slide-library items and translations. It preserves legacy inputs still consumed by the normal migration funnel and leaves unresolved slide types unchanged. Changed decks have their collaboration cache rebuilt when next opened. The migration logs each removed type/key pair; its down migration cannot restore removed values.
The unused payoff
taglineand per-slidecontent.videoOverrideare no longer read. Use the slide type’s declared fields and the configured live video layer instead. There are no new environment settings or package dependencies.Remove custom fields deliberately
Deleting or renaming a custom field that is in use first reports the affected content. API clients receive
409 field_in_usewith usage counts. Confirming removal, or using?force=true, strips the field from stored content and open collaboration documents in one operation, preserving other concurrent edits. Concurrent writes validate against the committed field definitions; content writes serialize within each organization, with throughput not yet benchmarked.Clearer labels across languages
Analytics metrics, custom slide types, corner shapes, appearance, typography and alignment use more consistent terminology. Moving a deck to the trash no longer claims the action cannot be undone. Layout tiles, theme cards and version actions also fit narrow screens more reliably.
For contributors, translation reviews now record which English source and translated value were reviewed, so changes can be identified as needing another review. This does not claim that all translations have been reviewed.
See the 1.91.0 release on GitHub for the complete changes.
-
v1.90.0
Send transactional email through SMTP, Brevo, Scaleway TEM or Mailgun. Existing Brevo installations need a configuration update before upgrading.
Deckyard can send invitations, sign-in links and notifications through SMTP, Brevo, Scaleway TEM or Mailgun. Select the transport explicitly and use the same sender settings across providers.
Update mail configuration before upgrading
Existing Brevo installations must add
MAIL_TRANSPORT=brevo, retainBREVO_API_KEY, renameBREVO_SENDER_EMAILtoMAIL_FROM, and renameBREVO_SENDER_NAMEtoMAIL_FROM_NAME. The previous names no longer work. Stored instance sender settings remain supported and take precedence over environment settings.Choose
MAIL_TRANSPORT=smtp,scalewayormailgunwith the corresponding provider settings, orMAIL_TRANSPORT=noneto disable outgoing mail. Credentials alone no longer select a provider. The configuration examples and doctor check explain missing or incomplete settings; the admin test-send uses the same effective sender as normal mail.SSO sign-in and existing passwords remain usable without mail. Invitations, notifications, password resets, magic links and new password-account setup require a working transport. An invitation now reports when delivery was unavailable or failed.
Forks and deployment
Forks importing the removed
server/integrations/brevo.jsmust switch toserver/integrations/email/index.js. There is no database migration or new package dependency. SMTP supports authenticated TLS connections; API transports use their fixed provider endpoints.Capture recipes have also been updated for the current editor, including translation demonstrations and five documentation views. This updates the capture tooling; refreshed website images are published separately after review.
See the 1.90.0 release on GitHub for the complete changes.
-
v1.89.1
Long image descriptions no longer block saving. Interrupted imports stop cleanly, deck descriptions are searchable again, and API and MCP theme lists follow organization settings.
This release fixes saving decks with long image descriptions and improves importing, search and theme selection.
Long image descriptions no longer block saving
An image alt text longer than the field limit no longer prevents the whole deck from being saved. Descriptions are shortened at a word boundary with an ellipsis, within the shared 180-character limit. This also applies to existing descriptions, nested image fields and translations. AI-generated alt text follows the same limit. Other field limits still apply.
Import, search and themes
- Interrupted file and Notion imports stop and return control to the form, without automatically sending another creation request when the result is uncertain.
- Metadata search finds decks by their description again.
- The public API and MCP offer the themes configured for the active organization, consistently with the editor. Existing decks keep their theme.
There is no new database migration or required configuration. See the 1.89.1 release on GitHub for details.
-
v1.89.0
Analytics and user management stay usable on narrow screens and with larger text. Forks importing two rendering helpers need to update their import paths.
Analytics and user management on smaller screens
Analytics headers, user-management actions and list pagers now wrap or stack when space runs out. Labels remain visible at 320 pixels wide with 200% text, and pager buttons have a minimum height of 44 pixels.
Breaking changes for forks
Update these imports if your fork uses the helpers:
- Import
escapeHtmlfromshared/slide-types/helpers.js; its re-export fromserver/utils/html-utils.jsis removed. - Import
themeVarsCssTextfromserver/utils/theme-vars-css.js; its re-export fromserver/utils/themes.jsis removed.
Renderer bundles: architecture proof
This release adds the architecture proof for frozen, content-addressed renderer bundles. Historical renderer execution remains disabled in production until an OS-isolated runner is available. Immutable publication editions are not enabled by this release.
There is no new database migration or required configuration. See the 1.89.0 release on GitHub for details.
- Import
-
v1.88.0
Removing a collaborator now removes their access to the deck’s analytics and weekly summary. The remaining lists load a page at a time. Forks using internal list endpoints or storage helpers need to update their calls.
Revoked collaborators lose analytics access
When a collaborator’s editing or admin access to a deck is revoked, that deck no longer appears in their analytics dashboard or weekly email summary. Both now use the same access check. Ownership still grants access; merely being able to view or comment on a deck does not grant a dashboard.
The remaining lists load a page at a time
Viewer sessions, analytics reports, the dashboard’s deck list, organization members and admin users now load in pages. Members and users appear newest first; the dashboard’s deck list puts the most recently changed deck first. The member list keeps its separate count and loads more without replacing the rows already on screen.
Breaking changes for forks
Forks calling the internal list endpoints must use
limitandcursor, then read the named list andnextCursor.offsetis refused andtotalis gone. The dashboard list also refusessort; rankings remain in the dashboard’s top presentations. Member counts have their ownGET /api/organizations/:id/members/countendpoint. The public v1 API and MCP contracts do not change in this release.Forks importing storage helpers also need to update:
- In
server/storage/analytics/index.jsor its concern modules,getViewSessionsForPresentationbecomeslistViewSessions, andgetPresentationsWithAnalyticsbecomeslistDashboardPresentations.ANALYTICS_LIST_SORTSis removed. - Those functions,
listAnalyticsReports,listOrganizationMembersandlistUsersrequire a page argument and return{ items, next }. parsePaginationParamsis removed fromserver/utils/request-validators.js. UseparseListPage, orparseListLimitfor a bounded search, fromserver/services/list-page.js.
For self-hosters
Migration 101 fills missing creation timestamps in analytics reports, organization memberships and users, makes those columns required and adds or replaces the indexes used for paging. It deletes no rows and runs at startup. Back up the database before upgrading; index creation can hold locks during the migration. There are no new dependencies or configuration settings. See the 1.88.0 release on GitHub for details.
- In
-
v1.87.0
Import a PowerPoint file and get editable slides, with or without its PDF export. Comments and the activity feed now load a page at a time. This release changes the comment endpoints of the public API and MCP: read the notes before upgrading an integration.
A PowerPoint file becomes editable slides
The import that turns a deck into editable slides now reads a
.pptxdirectly: headings, lists, tables, pictures and colours come from the PowerPoint file itself, inherited through its layouts and theme. Add the PDF export beside it and the PDF supplies the image of any page Deckyard cannot rebuild; the two files must have the same number of visible slides. A.pptxon its own works too: a page that cannot be rebuilt keeps its heading and text on a text slide, and the import report tells you that adding the PDF export would give you an image of those pages. Pictures stored as SVG, EMF or WMF are named in the report when they are left out.Comments and activity load a page at a time
The comments panel, the comments in a shared link and the Activity tab show a first page and load more as you go, instead of fetching everything at once. The activity feed now only holds events on decks you may open, and every page is full while there is more.
Breaking: comment lists in the public API and MCP
GET /api/v1/presentations/{id}/commentsand the MCP toolslist_commentsandlist_recent_commentsmove onto the page contract the other lists got in 1.85.0 and 1.86.0. Sendlimitand, for the next page, thecursoryou got back; the response is{ comments, nextCursor }, newest first, andnextCursorisnullon the last page. On a deck a page counts threads, each with all its replies.totalis gone, and so isoffset: a request withoffsetis refused with 400 and a hint to usecursor.limitmust be between 1 and 100. For the MCP tools that was 200.
An integration that read
totalor stepped through comments withoffsetneeds a change. The OpenAPI description and the MCP reference have the new shape.Smaller fixes
- The deck count on a tag in Presentations no longer includes decks in the trash.
- Importing a slide of a type this instance does not have keeps the text of every language version, lists included, instead of repeating the base language.
For self-hosters
This release runs migrations 099 and 100. 099 gives every activity event a creation time where it had none, makes that column required and replaces the index behind the activity feed. 100 does the same for comments and adds the two indexes behind the comment lists. Both run on their own at startup and need no action. No new dependency and no configuration change. See the 1.87.0 release on GitHub for the commit-level details.
-
v1.86.0
Home, shared-with-me, the trash, the slide library, collections and the image library now load page by page, with filters and search worked out by the server. This release changes the public API: read the notes before upgrading an integration.
Lists that load a page at a time
Home, shared-with-me and the trash used to load every deck at once and filter them in your browser. They now show a first page and a Load more button, and the filters, tags and search are answered by the server, so a large organization opens them as fast as a small one. Because the app no longer holds every deck, the counts on the filter chips, the deck count on Home and the sort by title are gone.
The slide library, collections and the image library work the same way. Their filter chips and tag lists come from the server, search runs there as you type, and a collection loads only the slides you are allowed to see. A selection you made in the picker stays when you search or filter.
Breaking:
GET /api/v1/slide-libraryandGET /api/v1/image-libraryBoth library endpoints move onto the page contract that
GET /api/v1/presentationsgot in 1.85.0. Sendlimitand, for the next page, thecursoryou got back; the response is{ items, nextCursor }for the slide library and{ images, nextCursor }for the image library, andnextCursorisnullon the last page. Items come newest first.offset,totaland thepaginationobject are gone. A request withoffsetis refused with 400 and a hint to usecursor.limitmust be between 1 and 100. A value outside that range is refused with 400 instead of quietly clamped.- The image
categorieslist is no longer part of the image-library response. AskGET /api/v1/image-library/categoriesfor it.
An integration that read
total,paginationorcategories, or stepped through pages withoffset, needs a change. The OpenAPI description has the new shape.For self-hosters
This release runs migrations 097 and 098. 097 adds an index behind the trash list. 098 gives every slide-library item, collection and image a creation time (its last-changed time, or now, where it had none), makes that column required, stores the uploader of an image in the same normalized form as every other address, and adds the indexes behind the three library lists. Both run on their own at startup and need no action. No new dependency and no configuration change. See the 1.86.0 release on GitHub for the commit-level details.
-
v1.85.0
The public API and the MCP server list decks page by page with a cursor, from one database query that decides who sees which deck. This release changes the public API: read the notes before upgrading an integration.
A deck list that holds up at any size
Which decks you see used to be worked out after the server had read every deck in your organization. One database query now decides it, ordered by last change with a fixed tie-break, so the list stays fast as an organization grows and two decks saved in the same moment always come back in the same order. A deck that is shared with you also keeps its sharing details in every list, including when it is visible to your whole organization as well. Nothing changes in the app itself in this release.
Breaking:
GET /api/v1/presentationsThe public deck list moves onto the same page contract as the rest of the API. Send
limitand, for the next page, thecursoryou got back; the response is{ presentations, nextCursor }, andnextCursorisnullon the last page. Decks come newest change first.offsetand thepaginationobject are gone. A request withoffsetis refused with 400 and a hint to usecursor.limitmust be between 1 and 100. A value outside that range is refused with 400 instead of quietly clamped.
An integration that read
pagination.totalor stepped through pages withoffsetneeds a change. The OpenAPI description has the new shape.MCP:
list_presentationsThe MCP tool
list_presentationstakes acursorand returnsnextCursorto fetch the next page. It no longer returnstotal.For self-hosters
This release runs migration 096: a deck without a last-changed time gets its creation time, that column becomes required, and the index behind the deck list is replaced by one that serves the new order per organization. It runs on its own at startup and needs no action. No new dependency and no configuration change. See the 1.85.0 release on GitHub for the commit-level details.
-
v1.84.0
The notification bell now has a Load more row, so older notifications stay within reach, and importing a deck whose theme uses a title slide this instance lacks no longer fails.
Load more in your notifications
The notification bell used to show your twenty most recent notifications and nothing older. It now ends with a Load more row that fetches the next twenty, in every lens (All, Unread, Mentions, Archived). The list stays in order and shows each notification once, also when new ones arrive while you scroll. Archive the notifications you can see and the next ones move up, instead of the bell claiming you have none left.
Importing a deck with an unfamiliar theme
Importing a
.deckfile whose theme opens new decks with a title slide this instance does not have used to fail with a server error. The import now succeeds: slides of an unknown type arrive as text slides that name the type, and a new deck in that theme opens on the standard title slide. An imported deck also no longer gets an extra title slide in front of its own slides.For API users and self-hosters
This release runs migration 095: it gives every notification without a creation time the earliest time it is known to have existed, makes that column required, and replaces the notification list index. It runs on its own at startup and needs no action.
The internal
GET /api/notificationsendpoint now pages with an opaquecursorand returnsnextCursor;offsetis refused with a hint to usecursor. The list filter has one form,filter=all|unread|mentions|archived: the retiredunread=trueand unknown filter values are refused instead of ignored. The public/api/v1is unchanged in this release. No new dependency and no configuration change. See the 1.84.0 release on GitHub for the commit-level details. -
v1.83.5
Importing a PDF as editable slides now follows the layout and background of each page, cuts a chart beside text out as a picture, keeps a table beside text as text and leaves out a logo repeated on every page.
Imported slides follow their page
When you import a PDF as editable slides, each slide now takes over the choices its page already made, as far as the slide type offers them. A picture beside text keeps roughly its width (narrow, half or wide), and a small picture in a corner stays in the corner. Text set in two columns becomes a two-column slide. A section title keeps its place at the top, middle or bottom of the page and stays centred when it was centred. A page with a dark or tinted background gets a matching background from your theme. Fonts and exact colours are not carried over: the slide stays in your theme.
Charts and tables beside text
A page with a heading, text and a chart or diagram used to become a picture of the whole page. When the chart stands apart from the text, it is now cut out of the page as a picture and the text beside it stays editable. A table beside text is no longer mistaken for a drawing: it becomes a text slide with the table in it.
Logos and other repeated marks
A small picture that sits in the same place on three pages or more, like a logo in a corner, is left out of the imported slides, and the import report says so once. It no longer turns every page into a picture beside text.
A clearer import report
A page whose picture could not be placed on a slide is now reported as too long, instead of becoming a text slide without the picture. Errors in the report now appear in your interface language.
Translation view
The background settings stored with a slide’s background image are no longer offered as text to translate and no longer count as missing translations.
For self-hosters
No migration, no new dependency and no configuration change. See the 1.83.5 release on GitHub for the commit-level details.
-
v1.83.4
Translating side by side now opens on the fields that hold text in at least one language. Empty rows stay out of the way until you choose "All fields", and the "No suggestion yet" placeholder is gone.
Only the fields worth translating
The side-by-side translation view used to list every field a slide could have, including the ones that were empty in both languages: a subheading you never filled in, an empty side note, an unused accessibility field. On a real deck that meant scrolling past rows with nothing to translate. The view now opens on a new default filter, “Fields with text”, which hides rows that are empty in both languages. Every slide stays in the index, so jumping to a slide works as before; a slide without any text says so. Choose “All fields” in the filter to see the empty rows again.
Less noise in the suggestion column
A field without a suggestion no longer shows “No suggestion yet”. The “Suggest field” button now has its own space in the cell, so it no longer crowds the suggestion text, also on a narrow screen or at a large zoom level.
For self-hosters
No migration, no new dependency and no configuration change. See the 1.83.4 release on GitHub for the commit-level details.
-
v1.83.3
With an S3-compatible bucket for media, the generated share preview of a published deck is now deleted from the bucket as soon as the publication no longer backs it: on unpublish, on republish and when you regenerate the preview.
A withdrawn preview is removed from the bucket
When you publish a deck, Deckyard generates a share preview: the image a link shows in chat apps and on social media. With the built-in local storage that image was already served only while the publication still stood. With an S3-compatible bucket, the bucket answers the image address on its own, so a preview stayed reachable after you unpublished the deck, republished it or regenerated the preview. Deckyard now deletes that object from the bucket as soon as the publication no longer backs it. It only touches previews it generated for that publication: an image you picked yourself and your other uploads stay where they are.
For self-hosters
No migration, no new dependency and no new or required configuration. If you store media in an S3-compatible bucket, remove the previews left over from before this release once: run
node scripts/sweep-published-previews.js --checkto see what it would delete, then--apply. A copy in a CDN or crawler cache is outside Deckyard’s reach. With the local provider there is nothing to do. Forks with their own media provider implement the newMediaProvider.withdrawPublicObject(url)method. See the 1.83.3 release on GitHub for the commit-level details. -
v1.83.2
In the translate view, a translation you type by hand no longer claims to be up to date with its source. Only an accepted suggestion marks a field "Current".
A typed translation does not claim to be current
The translate view shows per field whether a translation matches its source text. Typing in either column used to record the edit as if it were an accepted translation, so a hand-typed text could show “Current” without anyone checking it against the source. Typing now records nothing: only accepting a suggestion marks a field “Current”. A field marked “Source changed” stays so after you correct it by hand, until you accept a fresh suggestion for it, so the signal that the source moved on is not lost.
For self-hosters
No migration, no new dependency and no new or required configuration. See the 1.83.2 release on GitHub for the commit-level details.
-
v1.83.1
Duplicating a multilingual deck now names the copy a copy in every language version, and the translate view saves an edit to a source-language slide text even when that text has no translation yet.
A duplicate keeps its name in every language
Duplicating a deck named the copy “Copy of …” only on the surface. In a deck with more than one language version, the copy got its original name back as soon as it was opened or saved, so you ended up with two decks with the same title. Each language version of the copy is now named a copy in its own language (“Copy of …” in English, “Kopie van …” in Dutch), and that name stays.
The translate view saves source-language edits
In the translate view, editing a slide text in the source column did not save when that text had no counterpart in the other language yet, for example a field that was never translated. The edit now saves. Accepting a translation suggestion still records which source text it came from, and locked slides still refuse changes.
For self-hosters
No migration, no new dependency and no new or required configuration. See the 1.83.1 release on GitHub for the commit-level details.
-
v1.83.0
An existing API key now has an Edit permissions button in Settings → API Keys, so an integration can gain a new scope such as comments or collaborators without rotating its key.
Edit the permissions of an existing API key
Until now, the permissions of an API key were fixed when you created it. Every new opt-in scope, such as
comments:readorcollaborators:write, meant creating a new key and updating every integration that used the old one. Settings → API Keys now has Edit permissions on each active key: tick or untick the same permission boxes you saw when creating it and save. The key itself stays the same, and applications that use it get the new permissions on their next request.The change is recorded in your activity feed on Home, with the permissions that were added and removed. Integrations can make the same change through the API with
PATCH /api/api-keys/:id. On a narrow screen the key’s actions now wrap as whole buttons instead of squeezing their labels.For self-hosters
No migration, no new dependency and no new or required configuration. Behind the scenes this release also fixes the screenshot recipe for the deck settings page and lets CI skip its follow-up jobs when a run is cancelled. See the 1.83.0 release on GitHub for the commit-level details.
-
v1.82.0
The sandbox banner now points to installing Deckyard yourself and to hosted Deckyard; inserting a slide between two others happens from a thin seam, and the thumbnails in the slide drawer fill their cards at every width.
From trying to self-hosting
The banner on the public sandbox used to say only when your work would expire. It now also shows the next step: Install Deckyard yourself opens the self-hosting quickstart in a new tab, so your sandbox work stays where it is, and Or let us host it for you opens a mail to ask about hosting. Both links appear on Home and in the editor. The audience never sees them: presenting, the present window and follow-along stay free of the banner. On a narrow screen the two links stack, each with a full touch target.
Insert between slides from a thin seam
Between two slides in the editor’s slide list there is now a thin seam instead of a fixed strip. Hover it and the neighbouring slides shrink for a moment to make room for a fully visible
+; click it and the new slide lands exactly there. The list wastes no space when you are not inserting, and the touch and rail views work as before.Thumbnails that fill their card
Slide thumbnails in the drawer are now sized from the width they actually get on screen. They fill their card at every window width, after you drag the drawer wider or narrower, at 200% zoom and in the narrow drawer on a phone, instead of leaving an empty margin or being cut off.
For self-hosters
No migration, no new dependency and no new or required configuration. The sandbox links only appear on an instance that runs with
SANDBOX_MODE. See the 1.82.0 release on GitHub for the commit-level details. -
v1.81.0
An AI agent connected through MCP can now see who has private access to a deck and give one colleague access, without publishing the deck; both tools need a permission you grant explicitly on the API key.
Share a deck privately from an agent
Two new MCP tools do for an agent what the share dialog in the editor does for you.
list_collaboratorsshows who has private access to a deck, with name and permission.add_collaboratorgives one existing member of your organizationview,comment,editoradminaccess.Both follow the same rule as the editor: only someone who may manage a deck’s collaborators (the owner, the creator or an
admincollaborator) may read the list or add to it. The agent acts as the person whose session it runs in, never with the rights of whoever owns the API key.- Private stays private. Adding a collaborator changes neither the deck’s visibility nor its share links. Publishing is a separate action.
- Only existing colleagues. The invitee must already be a user in the deck’s organization. Unknown addresses, people from another organization and your own address are refused, and nothing is written.
- Repeating is safe. The same call twice changes nothing the second time. Someone who already has a different permission keeps it; the tool never changes an existing permission.
- Mail only on request. Every grant shows up as an in-app notification and in the deck’s activity, as in the editor. The invitation email goes out only when the agent asks for it with
sendInvitation: true.
New API key permissions
The tools come with two new permissions on an API key:
collaborators:readforlist_collaboratorsandcollaborators:writeforadd_collaborator. Managing who has access is a right of its own, so a key that may already edit decks cannot share them until you grant these in Settings → API keys. Existing keys gain nothing, and an agent only sees the tools its key may call.For self-hosters
No migration, no new dependency and no new or required configuration. The MCP server now offers 29 tools. See the 1.81.0 release on GitHub for the commit-level details.
-
v1.80.0
Deck settings open as a side panel in the editor with its own address, next to the slide preview; the share-link verification log, which stored the IP address and user agent of every visitor, is gone.
Deck settings in a side panel
The settings of a deck are no longer a dialog. They open from the editor’s ⋯ menu as a panel on the right, while the slide preview stays visible next to it. On a narrow screen the panel opens as a drawer.
The panel has its own address. Reloading the page, back and forward bring you to the same panel and section, and a link to it works, also with a language route in the URL. The source language of a deck is shown read-only in the panel. Stored values and server-side permissions do not change.
Share-link verification log removed
Until now, every successful verification of a share link wrote the visitor’s IP address and raw user agent to a separate log, outside the analytics switch and without any cleanup. Nothing in Deckyard read that log. Writing to it has stopped, and the internal route that could read it,
GET /api/presentations/:id/share-links/:linkId/access-log, is gone.Share links still keep their own usage count and last-used time, and analytics remains the only record of who viewed a deck. This is the second step of the privacy pass on visitor data that started in 1.79.0; the rest of that work is not part of this release.
For self-hosters
This update has one SQL migration, 094. On upgrade it drops the
share_link_access_logtable with all its rows; afterwards the database counts 94 migrations. The data cannot be brought back: rolling the migration back recreates an empty table. If you or your fork read that log, export it before you upgrade. Make a backup first, as with every migration.No new dependency and no new or required configuration. See the 1.80.0 release on GitHub for the commit-level details.
-
v1.79.0
Every settings section has its own address and unsaved changes ask before you leave; notifications about attempts on a revoked link no longer contain the visitor IP, and the attempt record lasts 24 hours.
Settings you can link to
Every section of the settings menu now has its own
#address. Back and forward and reloading the page bring you to the same section, and a link to a section opens that section. On narrow screens, one button with the name of the current section opens the list, instead of a horizontal strip.A form with unsaved changes now asks for confirmation before you switch section or go back to the overview. Slide type curation has become an ordinary form with a save button instead of saving automatically. Server-side permissions and stored values do not change.
Revoked links: less data, kept shorter
When someone tries to open a share link that has been revoked, the author still gets a notification. That notification no longer contains the visitor’s IP address, and the record of the attempt only exists for the 24 hours in which duplicate notifications are suppressed; the daily cleanup removes older records.
This is the first step of a wider privacy pass on visitor data. The rest of that work is not part of this release.
For self-hosters
This update has one SQL migration, 093. On upgrade it deletes old attempt records and removes the IP address from existing notifications; afterwards the database counts 93 migrations. The migration logs two counts,
access_attempt_log: N row(s) past the window deletedanduser_notifications: N access_attempt row(s) rewritten without accessorIp, which tell you how much was cleaned up. Make a backup first, as with every migration.No new dependency and no new or required configuration. See the 1.79.0 release on GitHub for the commit-level details.
-
v1.78.0
Settings are grouped by who they affect, each form saves one thing, and public preview images now follow whether a deck is published; old preview files are no longer served.
Settings grouped by scope
The settings are now grouped by who a setting affects: personal, organization, design and installation. Routes and stored values stay the same, so links and existing configuration keep working. Policy actions are only shown to organization admins and installation forms only to instance admins. Each form saves one thing: the sender address and the webhooks, for example, each have their own save button now.
Server-side permissions have not changed. What changed is the menu: you see the settings you can actually change, in the place where you would look for them.
Public previews follow publication
Public preview images (share cards and thumbnails) now follow the deck’s publication choice. Preview files generated earlier are no longer served, and an ordinary save of a deck no longer shifts its publication state. Republishing a deck or regenerating its preview makes a new image.
With this, the reader protection from the 1.77 releases also covers published previews. Withdrawing previews that another origin has already fetched is separate work and not part of this release.
For self-hosters
No SQL migration, no new dependency and no new or required configuration. After the update, decks whose old preview is no longer served get a new one when they are republished or their preview is regenerated. See the 1.78.0 release on GitHub for the commit-level details.
-
v1.77.5
Exports made by readers, including queued background jobs, no longer contain speaker notes or slides hidden from viewers; notes-only downloads are for authors.
Deckyard 1.77.5 extends the reader protection to exports. When someone without write rights on a deck exports it, through the app or the public v1 API, the file contains no speaker notes, no AI reasoning fields and no slides hidden from viewers. That holds for every format, including portable JSON,
.deckfiles and handoff packages, and for every language version in the deck. Owners and editors with write rights keep their existing export content.Notes-only downloads (Markdown and Word) are now for authors only; readers get a refusal. Exports that run as a background job remember who asked for them and with which rights. Those rights are checked again when the file is built and again when it is downloaded: someone who lost write rights in between gets the reader version or a refusal, and a finished file is only handed to the person who requested it. Older queued results without this access information are refused, so they have to be exported again.
Published pages and preview images (share cards and thumbnails) are not covered yet and should not be treated as protected by this change. The reader data reference lists the exact boundary.
There is no SQL migration, new dependency or required configuration change for this update. See the 1.77.5 release on GitHub for the commit-level details.
-
v1.77.4
People following a presentation no longer receive speaker notes or slides hidden from viewers, in the follow-along view, Q&A, interactions and live updates.
Deckyard 1.77.4 extends the reader protection from 1.77.3 to follow-along presenting. Someone following a presentation now receives the audience version of the deck: no speaker notes, no AI reasoning fields and no slides marked as hidden from viewers or hidden in the presentation. This applies to the follow-along deck data, server-rendered slides, Q&A and interaction state and both live update streams. When the presenter is on a hidden slide, followers see a neutral waiting view instead of that slide.
The follow-along responses no longer contain the live-session id that gives access to the notes companion; that id now stays on the server. The presenter and the notes companion keep their author view. A companion link that was already handed out stays valid for the existing session lifetime. This release also fixes race conditions when a live update stream disconnects.
Exports (including queued jobs and notes downloads) and published pages and preview images are not covered by this release and should not be treated as protected by it. The reader data reference lists the exact boundary.
There is no SQL migration, new dependency or required configuration change for this update. See the 1.77.4 release on GitHub for the commit-level details.
-
v1.77.3
Readers no longer receive speaker notes or slides hidden from viewers when a deck is loaded as JSON or through MCP.
Deckyard 1.77.3 narrows what a reader receives when a deck is loaded as data. Someone without write rights on the deck no longer gets speaker notes, AI reasoning fields or slides marked as hidden from viewers. This applies to the internal deck API, the public v1 API for decks and single slides, share links and the MCP read and preview tools, and to every language version a deck carries.
Who counts as a reader is now decided by the actual write rights on the deck. An editor who cannot write to a particular deck therefore receives the reader version as well; previously that route still returned the author data. Owners and editors with write rights keep their notes, also when their API key only has
readpermission.This fix covers the JSON and MCP surfaces named above, not every route yet. Follow-along presenting, exports (including queued jobs and notes downloads) and published pages and preview images are not covered yet and should not be treated as protected by this change. The reader data reference lists the exact boundary.
There is no SQL migration, new dependency or required configuration change for this update. See the 1.77.3 release on GitHub for the commit-level details.
-
v1.77.2
Collaboration now rejects browser connections from an unexpected website, while connections from your Deckyard site continue to work.
Deckyard 1.77.2 tightens the connection check for live collaboration. A browser trying to open a collaboration connection from a different website now receives a 403 response, even if it has a valid session cookie. Connections from your Deckyard site’s own address continue to work.
The check also works when Deckyard runs behind a supported reverse proxy. Native clients and server integrations that connect without a browser
Originheader remain supported, subject to their usual authentication and permissions.There is no SQL migration, new dependency or required configuration change for this update. See the 1.77.2 release on GitHub for the commit-level details.
-
v1.77.1
Large decks now translate in saved batches, so a later error does not discard completed work. The editor uses one translation view, and older decks keep their intended background text appearance when opened.
Translating a large deck is more resilient in 1.77.1. The editor also brings translation into one view, while a fix preserves the appearance of older slides that used a historical background text setting.
Large decks keep completed translations
The direct translation API and its worker now translate a deck in batches and save each completed batch. If a later batch fails, the text already translated stays in the deck. A partial response (
502 translation_partial) tells API clients what remains; retry the same request withfillMissing: trueto fill the gaps. Edits made to the deck while translation is running are retained.One translation view in the editor
The language menu and a slide’s context menu now open the same side-by-side translation view. The separate actions for translating a whole deck, one slide or one field have been removed; you can work through those texts in the shared view.
Older background text settings render as intended
Older decks that stored
defaultfor background text colour keep their original appearance when opened, including in live collaboration. Deck schema 24 normalizes that historical value automatically. Current valid settings are unaffected.For self-hosters, no SQL migration, new dependency or configuration change is needed. See the 1.77.1 release on GitHub for the commit-level details.
-
v1.77.0
Core now ships capture recipes for the three ways to start a deck: Blank, Generate from content and Import an existing deck. Nothing changes in the app itself.
Deckyard 1.77.0 adds three screenshot recipes to core, one for each way to start a new deck: Blank, Generate from content and Import an existing deck. A recipe sets up the dialog in a known state with demo data, so the documentation images of these views can be captured again from any release instead of being taken by hand and slowly going out of date.
This release only makes the recipes available. Capturing the new images and replacing the outdated ones on this site is a separate delivery, independent of this release.
There are no changes to the app, no SQL migration, no new dependency and no configuration change. The number of built-in slide types has not changed. See the 1.77.0 release on GitHub for the commit-level details.
-
v1.76.0
A report or a Word document now reaches the AI as a document instead of a wall of text: headings, lists and tables survive, footnotes and running headers do not, and a page in two columns is read column by column. Pictures in the source become candidates the AI can place on a slide, and the translation view gained slide navigation, previewed suggestions and a per-field record of which source text a translation was made from.
A source arrives as a document
Generating a deck from a report used to flatten it: every page became a block of text, headings lost their rank, paragraphs broke at the page edge and the running footer came along on every page. A PDF now travels as a marked-up document. Headings are ranked by how they look against the body text, the cover becomes the title, paragraphs are joined across page breaks, lists stay lists and data tables stay tables. Recurring margin text is dropped, and so are footnotes and the reference marks that call them.
A page set in two columns is now read column by column instead of line by line across both, so a key-data table no longer interleaves with the commentary beside it. Word documents keep their heading levels and their lists, nested items included, and are sent as one continuous document.
Two things deliberately stay as they were. A landscape page with little text is still treated as a slide, not a document, and the slide import still reads rows rather than columns: boxes side by side on a slide are layout, not a text column.
Pictures from your source
A picture in your source document never used to reach the deck: a PDF’s figures were skipped, a Word document’s fell away with its markup, and a Notion page put every image but the first at the end. Each source now marks its pictures where they stand and offers them as candidates. The AI ties them to sections and places at most one on a content slide; only the pictures a slide actually uses are uploaded.
Page furniture is left out: icons, letterhead logos, backgrounds behind text and pictures that repeat on several pages. A Word document keeps pictures of 40 pixels a side or more, and a Notion page’s first-section image becomes the title background.
Translating: navigate, preview, and see what changed
Three additions to the side-by-side translation view from 1.75.0:
- Slide navigation. You can move through the deck inside the view, and the slide you are typing in stays the selected one. Scrolling with the mouse still selects what you look at.
- Suggestions before you accept them. A third column can propose a translation for a field, for a slide, or for every empty field at once. A suggestion stays local until you accept it; accepting in bulk only fills fields that are still empty. If the source or the target changed since the suggestion was made, it is held back and you can ask for a fresh one. These controls follow your AI setting.
- Source changed. A translated field now records which source text it was made from. Edit that source in the ordinary editor and the translation is marked Source changed, which survives saving, reloading, undo and redo, and edits by other people in a live session. You can filter the rows on it, and on translations that are still identical to their source.
Also fixed
- Chart labels in a tagged PDF come out once. A chart exported by Deckyard and imported from a tagged PDF repeated every label in its group, so an axis read
555555555. Each label is now read once. - An icon name is no longer counted as untranslated text. The name of an icon is a lookup key, not prose, but the language menu counted it as missing and the translation view offered a row for it. Machine translation would have translated the key and left the card without its icon. Four other fields of the same kind were affected: a media library id, a serialized coordinate list and two slide references.
For self-hosters and forks
No SQL migration and no new settings. The stored deck schema moves from 22 to 23 for an optional per-field record of translation provenance; there is no backfill, and an older deck opens unchanged.
deckyard doctornow reports a thumbnail cache it cannot write to, which used to surface as thumbnails that silently never appeared.pdf-parseis gone from the optional dependencies: both the AI route and the slide import read PDFs through the same reader now.For forks: a field that holds prose the reader does not print, such as an interaction’s input hint, declares
interactionCopy: true; it is the opposite claim topresentational: true(machine data), and declaring both is a warning. The public v1 slide-type catalogue now includes the organization’s own slide-type definitions. The number of built-in slide types has not changed. Details in the release on GitHub. -
v1.75.0
A new translation view shows every text of a deck in two languages side by side, both editable, so you can translate a whole deck without switching versions. AI-generated slides are short again, with the explanation in the speaker notes, and a security fix keeps the AI notes per slide with the people who can edit the deck.
Security: the AI notes per slide stay with the author
A deck made or imported with AI keeps, per slide, the AI’s reasoning, alternatives and warnings for the review grid. That text could reach people who may only view the deck: readers, guests on a share link (also with a password), the follow-along page and the public API. It can contain text from the source document. Only people who can edit the deck now receive it. Live editing also checks access again while a connection stays open, so someone whose access is taken away stops receiving changes within seconds. Self-hosters should update.
Translate side by side
Translate side by side… in the language menu and in the slide’s context menu opens a full-screen view with every text of the deck as rows: source on the left, translation on the right, both editable. A filter shows only the fields that still need a translation. With more than two languages you pick the pair. In a live session, edits by others and undo and redo show up in the view, and a slide someone else is editing stays locked.
Shorter AI slides
AI-generated slides had grown into walls of text. A slide now carries the core in about sixty words, with one short line per item, and the explanation goes to the speaker notes. Generating a deck with Claude also costs noticeably less, because the fixed part of the instructions is cached between calls.
For self-hosters and forks
No migration and no new settings. Live editing now refuses people who cannot edit the deck; viewers never used it. For forks: text in any language version is written through
writeVersionText(client/views/editor/version-text.js), andslideTextRowsinshared/i18n-progress.jswalks a deck’s text in two languages. An AI message can carrycache: true. Details in the release on GitHub. -
v1.74.0
Importing an existing deck can now turn a slide PDF into editable slides instead of images, with its text taken over word for word. Every AI route (the wizard, file and Notion imports, the API and MCP) now runs one generation pipeline, comments show clearly whether they belong to the slide or to a spot on it, and a security fix makes a share link's password guard its guest discussion too.
Security: a share link’s password guards the guest discussion
A password-protected share link with comments turned on let a guest join the discussion without giving the password. A guest now has to pass the password before they can comment, and a revoked password link no longer names its deck. A password-protected link pasted into a chat or a mail also unfurls without the deck title. Self-hosters who use password links should update.
PDF as editable slides
Import an existing deck → PowerPoint or PDF now offers As editable slides (the default) next to As images. A slide PDF becomes real slides with titles, text and lists you can edit, taken over word for word without AI. A page that does not translate well, such as a page with a chart or a drawing, stays an image slide, and the import report tells you which pages did and why.
One pipeline behind every AI route
The creation wizard, file and Notion imports, the public API and MCP now generate a deck the same way. Imported decks get speaker notes, the model sees the theme’s colours, the title slide follows the theme, and the review grid shows alternatives and warnings for every route. All imports show the same progress steps, in the language you chose. When generation fails, you now see the error instead of a silently weaker deck.
Comments
Comments on the whole slide and comments on a spot of it now look different. An empty comment pane offers both (“Comment on this slide” and “Mark a spot”), a pinned comment gets a pin icon and a Show on slide action, and you can place a pin with the keyboard. Readers and commenting guests see the pins too. A comment marker names its author and how the comment starts, instead of “undefined”.
Language
In a Dutch deck, the empty video and embed slides, the countdown buttons and the accessible names of cycle, funnel, process and pyramid slides now speak Dutch.
For self-hosters and forks
No migration and no new settings. For forks:
generateDeckV2is nowgenerateDeck,reattachAiMetais replaced bygeneratedDeckToParts(both inserver/utils/ai/index.js, no aliases),loadAiThemeContextmoved toserver/utils/themes.js, andserver/utils/openai/deck.jsis gone. The import progress no longer sends the phase wordsparse,analyze,convertandfetch. The slide-import client seam is nowcreateDeckFilesPicker/importDeckFiles, and there is a new endpoint,POST /api/presentations/import/slides. Details in the release on GitHub. -
v1.73.0
Creating a deck now offers two routes before you pick a file: generate a new deck from content with AI, or import an existing PowerPoint or PDF one to one as image slides, without AI. The theme choice shows each theme as the real title slide your deck starts with, the share dialog opens on Guests, and a translated deck keeps the language version you are working in.
Generate or import
The creation view asks what you want before it asks for a file. Generate from content reads a text, a document or a Notion page and writes a new deck with AI. Import an existing deck takes a PowerPoint, a PDF or both and turns every page into an image slide with its speaker notes, in the theme and language you choose. The import works without AI, so it is also available on an install with AI turned off.
.deck, JSON and Markdown imports sit next to it as before.One card per theme
When you start a deck, each theme shows the title slide the deck will actually get, photo included, both in the creation view and on Home. The workspace default is no longer a separate card but a badge on its own theme. A new deck, and a title slide you insert in the editor, now gets the theme’s first background preset instead of a random one; existing decks are unchanged.
Sharing
The share dialog opens on Guests, with a line above the tabs that says where the deck stands (“Private · 2 guest links · Not published”). New comment links are open to anyone with the link by default; existing links keep their setting. Label and password sit under “More options”, and every link says when it expires. A withdrawn, expired or used-up share link no longer shows the deck title when pasted into a chat or a mail.
Translated decks
A deck with several language versions keeps the one you are working in when an import, an applied comment, a promoted question, a publish or a restore writes it back. A change made through the API or by an agent without language data lands in the main language version, so the editor no longer shows the old deck after it.
Editor fixes
- The slide menu follows the keyboard: arrow keys move through it and focus returns to the slide after an action.
- At exactly 1024 pixels wide (an iPad in landscape) the editor shows the full desktop layout with the inspector; the collapsed slide rail shows one number per slide.
- When a colleague pastes slides halfway through a deck, they appear in the right place in your tab without a refresh.
- An empty background shows the slide type’s own standard (or the theme’s), so “Back to default” no longer stores a value.
- On an image-text slide the image width has one control, the split tiles.
- Clearer field names: “Detail line” (was Meta), “Side note” (was Aside) and “On your phone” (was Notes (QR)); chart and video fields no longer list formats in their label. A handful of tooltips and labels that stayed in English now follow the interface language.
- AI import keeps a list to its own items; a remark about the list moves out of it.
For self-hosters and forks
No migration and no new settings. For forks: the
lgbreakpoint now sits atmax-width: 1023px/min-width: 1024px(was 1024/1025), so a fork stylesheet using the old boundary should move with it. The slide import UI moved toclient/views/slide-import/, and there is a new endpoint,POST /api/presentations/import/images./api/slide-typesnow carriesautoBackgroundPreset. Details in the release on GitHub. -
v1.72.0
You can now work through the editor with the keyboard: the slide list is one Tab stop with arrow keys, canvas fields sit in the Tab order and every stop shows the same focus ring. The inspector marks the default of each setting and offers "Back to default". Long exports, imports and AI tasks show their progress in one dialog you can cancel, and HTML exports with many photos are a fraction of their former size.
The editor by keyboard
The slide list is a single Tab stop: arrow keys, Home and End move between slides, Left and Right fold and unfold a section, and Enter, the menu key or Shift+F10 open the actions of the focused slide. From there, Tab reaches every field on the canvas in the order of the slide, Enter starts editing it and Escape or Enter put you back on the field. Empty fields and the ”+” chips for missing ones have a name a screen reader reads out, and every stop shows the same focus ring in light and dark mode.
The default, and the way back to it
Each setting in the inspector marks its default option with a dot and “(Default)” in its tooltip. As soon as a setting deviates, a “Back to default” button appears next to it. This replaces the separate “Default” option under image fit and the “Center” button under focus.
A calmer inspector
While a slide’s image is still empty, the image card is the first row of the inspector, so you add the picture before choosing a layout; once it holds an image, Layout comes first again. Each image has one card. Block alignment under “This text” is no longer a greyed-out control but a line that says what the block is set to, with a button to the Layout row. Each scope has one label, and the literal “null” above the image library search is gone.
Long tasks you can follow and cancel
PDF, PNG zip, both PowerPoint exports, the handoff zip, slide import and AI analysis show their progress in one dialog (“Slide 13 of 54”) with a Cancel button. Cancelling stops the work on the server too.
Smaller exports
The HTML export, the published
/p/page and the PNG preview now downsample photos the way the PDF route already did. A test deck of 54 photo slides went from over 300 MB to about 50 MB for the HTML export.Editing fixes
A style or layout change made while you edit text inline shows on the canvas at once, and a click in the inspector right after typing on the canvas lands on the first try.
For self-hosters and forks
No migration and no new settings. Decks or library slides that still name a theme by its old slug (an install that never ran
scripts/migrate-theme-references.js) now produce a warning at boot, andnpm run doctorturns red on the newtheme-referencescheck with the count and the command to fix it. Creating a library slide with an unknown theme is refused. Details in the release on GitHub. -
v1.71.0
The editor flow is complete: edit a slide as a form in the Fields tab, find Layout at the top of the inspector and the slide actions in the slide list. The slide picker shows one tile per type and opens on "Start here". Exports and deck thumbnails recover by themselves when the headless browser fails.
Fields: a slide as a form, next to the canvas
The Fields tab over the canvas column shows the content of the selected slide as a form, and the slide updates while you type. It replaces the “All text” dialog, which no longer exists. Settings live only in the inspector, so content and settings no longer share a screen. The slide library edits its slides with the same form.
Layout first, slide actions in the slide list
Layout is the first row of the inspector. The type pill at the top is a menu for changing the slide type. Duplicate, copy, paste, hide and delete moved to the menu in the slide list and on each thumbnail, where you are already looking when you pick a slide.
Chart data edits live
The chart data editor updates the slide while you edit and closes with Done. The whole session is one undo step, and the form shows a summary of the data instead of the grid.
A calmer slide picker
The picker shows one tile per slide type; types with variants (callout, image + text, list) fold them into a row under the tile. Without any history it opens on “Start here”, eight types to begin with. The Media shelf reads in two rows, “One image” and “Collection”, and “Image blocks” is now called “Image cards” (same type, same id). The “From your library” strip is gone; the same slides are in the Slide library tab.
Exports and thumbnails that recover
Every download in the export dialog fetches its file in place: no blank tab while a large PowerPoint or handoff zip builds, a busy button meanwhile and a readable error under the row if it fails. When the headless browser crashes or fails to start, the next export launches a fresh one without a server restart. Deck cards on Home and Presentations show their thumbnail, or their placeholder at once when a render fails, instead of an endless shimmer.
For self-hosters and forks
No migration. One new optional setting,
EXPORT_BROWSER_LAUNCH_TIMEOUT_MS(default 30000,0disables the cap). A browser that cannot start now answers503 browser_unavailableinstead of500 export_failed, andnpm run doctorlaunches the browser for real, so a Chromium that crashes on start shows up red. Forks with editor customisations should check the release on GitHub: thebulksurface andbulk-edit-modal.jsare gone,bulkOnlyFields()is nowfieldsOnlyFields(), and export-modal rows describe themselves withprevieworfileinstead ofopen. -
v1.70.0
The editor gets tabs over its two working columns, a drawer for the inspector on smaller screens and Tab to move between fields on the slide. Importing slides as images keeps the speaker notes, taken from the PowerPoint file you add beside the PDF.
Tabs over the canvas and the inspector
The two working columns of the editor each have their own tabs: Canvas | Fields over the slide, Inspector | Comments at the head of the right-hand column. Panels, stage and top bar each get their own tone, so you can see at a glance where you are working.
A drawer for the inspector at 1024 pixels and below
On a laptop or tablet the inspector used to sit under the canvas and the slide shrank to a stamp. At 1024 pixels and below it now opens as a drawer over the right edge of the canvas, and the slide keeps its height. Escape, the close button or the selected tab closes it again.
Tab through the slide
While you edit text on the canvas, Tab and Shift-Tab move to the next or previous field of the slide, including the empty ones you can still fill in. The suggestions for empty fields stay visible while you type, not only on hover.
The image comes first on image slides
On an image slide or an image-with-text slide the inspector opens on the image itself: choose or upload when it is empty, and the thumbnail, Replace and the alt text right under it when it is filled. Settings that only make sense for a picture wait until there is one. The alt text shows a warning when publishing would refuse it. The image width is named the same everywhere: 37% · 50% · 63%, small to large.
Speaker notes when importing as images
“Import slides as images” now keeps your speaker notes. Add the PDF your presentation app exported together with the PowerPoint file: the pixels come from the PDF, the notes from the .pptx, paired by slide order. If the two files count a different number of slides, the import says so and writes nothing. A PowerPoint import also comes out in the order of the deck again, with each slide’s own notes, even after slides were reordered in PowerPoint.
No migration and no new configuration. For the commit list, see the 1.70.0 release on GitHub.
-
v1.69.1
Importing a PDF as slide images works again on servers whose browser has no built-in PDF viewer, such as the Docker image. Transferring ownership of a deck now accepts the new owner's address however you capitalise it.
Import a PDF as images
“Import slides as images” failed on every PDF in the Docker image and on any other install whose headless browser has no PDF viewer: the browser treated the file as a download and the import stopped with an error. Deckyard no longer opens the PDF as a page; it counts and renders the pages directly. A failed or cancelled import also no longer leaves a page open in the shared browser.
Ownership transfer and e-mail casing
Since 1.69.0 the server stores every e-mail address in one spelling. The editor now compares addresses as the server sends them, and folds only the one address you type yourself: the new owner when you transfer a deck.
Ann@Example.orgfinds the collaboratorann@example.org.No migration and no new configuration. For the commit list, see the 1.69.1 release on GitHub.
-
v1.69.0
A fresh install creates its first admin from AUTH_ADMIN_EMAIL, so you no longer have to switch sign-in off to get started. An e-mail address now finds the same user however it is capitalised. Hidden slides stay hidden in every language version, and file, Notion and Markdown imports check slide fields first. Forks update one import path; migration 092 rewrites stored addresses.
A first admin on a fresh install
Set
AUTH_ADMIN_EMAILand start Deckyard on an empty database: it creates that address as the first admin and logs how to sign in, by magic link or SSO. You no longer needAUTH_ENABLED=falseto get past an empty user table. Once any user exists, the variable does nothing. When neither e-mail delivery nor SSO is configured, the log warns that the admin has no way in yet.One spelling per e-mail address
Ann@Example.organdann@example.orgare now the same person everywhere: sign-in, sharing, favourites and the personal slide shelf. Addresses are folded in one place, and migration 092 rewrites the casing of addresses already stored. If two stored rows would collide on a unique column, the migration leaves them as they are and writes awarnline to the deploy log, so read that log after upgrading.Hidden slides stay hidden in every language
A slide hidden from viewers was left out of the deck’s main language, but still reached a viewer or commenter through the other language versions of the deck data. It is now left out of every version.
Imports check slide fields first
File conversion, Notion import and Markdown import now run through the same field check as the editor and the JSON import. An invalid value is refused with the language, slide and field named, before any deck is created.
Breaking changes for forks
Module Change server/utils/normalize.jsNo longer exports normalizeEmail; import it fromshared/email.jsMigration 092 Lowercases stored addresses in users, user_settings, presentation_collaborators, presentations.trashed_by, presentation_versions, slide_library, slide_collections and view_sessions; collisions on a unique column are left in place with a warnin the deploy logNew configuration:
AUTH_ADMIN_EMAIL(optional). See the 1.69.0 release on GitHub for the commit list. -
v1.68.0
A link to slide 3 now leads to slide 3 everywhere: in the presenter, the reader, PDF, PPTX and exported decks. Imports refuse invalid slide fields before a deck is created, and request limits share one token bucket across instances. Forks update the Redis rate-limit call and one projection option.
A slide jump means the same slide everywhere
A button or link that points to
#3now opens the third slide of the deck in every place it can be followed: the presenter, the reader, the view-only page, print and PDF, and both PPTX exports. Before, some of those counted only the slides they happened to show, so a skipped or hidden slide earlier in the deck sent the jump one slide too far. A jump whose target is not shown renders no link instead of a wrong one.Exported decks write in-deck jumps in that same positional form, counted on the exported slide order. A jump whose target is left out of the export is dropped, so the exported deck imports cleanly again.
Imports check slide fields first
Creating a deck with slides, importing JSON and importing a
.deckbundle now run every language version through the same field check the editor uses. An invalid value is refused with the language, slide and field named, before any deck is created, so a failed import leaves nothing half-made behind.One request limit across instances
Rate limiting now uses a token bucket that behaves the same in memory and in Redis, with one atomic step per request. Self-hosters running several instances behind Redis get one shared limit instead of slightly different counts per backend. Creating users as an admin follows the same limiter: a burst of 20, then one more every three minutes.
Breaking changes for forks
Module Change server/utils/rate-limit-redis.jscheckRateLimitRedis(key, { capacity, windowSeconds })is removed; useallowRequestRedisDetails(key, { capacity, refillPerSec }, runWithRedis?). Keys live underratelimit:token-bucket:; old keys expire on their ownshared/slide-types/semantic-projection.jsThe projection option slideIdsis renamedslideJumps({ deck, document }, fromslideJumpsFor())server/export/pptx.jsbuildPptxBufferandbuildEditablePptxBuffertake the stored deck and leave live-only slides out themselves; pass the unfiltered deck. View-only responses pin jumps withpinSlideJumps()fromshared/slide-types/deck.jsThis release adds no database migration or new configuration variable. See the 1.68.0 release on GitHub for the commit list.
-
v1.67.0
No change for people using Deckyard. Every storage call that reaches the database now takes a scope first, and the database infrastructure moves to server/db. Forks must update seven calls and five import paths.
Every storage call now names its scope
This release finishes the move to scope-first storage that 1.65.0 and 1.66.0 started. Every exported storage function that reaches the database now takes a
StorageScopeas its first argument: either the organization it acts for, or an explicit instance scope with a reason. Nothing changes in the editor, the API or exports. For self-hosters it means one rule now holds across the whole storage layer, so a call that forgets which organization it serves cannot slip through.Breaking changes for forks
Seven domain exports take
StorageScopeas their first argument. Insert it before every previous argument; the old signatures are removed.Module Change server/storage/activity-events.jsdeleteOldActivityEvents(scope, olderThan): instance retention scope onlyserver/storage/slide-locks.jscleanupExpiredSlideLocks(scope): instance retention scope onlyserver/storage/themes.jslistSeedThemes(scope): organization or reasoned instance scopeserver/storage/settings.jsresolveSeedThemeSlug(scope, slug),installationDefaultThemeId(scope): organization or reasoned instance scopeserver/storage/tags.jsreplaceTagLinks(scope, { linkTable, rowId, tagNames }): theorgIdfield is removedserver/storage/utils/index.jsgetUserIdByEmail(scope, db, email): theorgIdargument is removedThe database infrastructure leaves
server/storage/, without re-exports at the old paths:Export Old path New path withDbGuardserver/storage/utils/db-guard.js(also re-exported byserver/storage/utils/index.js)server/db/client.jsinitializeStorage,closeStorageserver/storage/lifecycle.jsserver/db/lifecycle.jspendingMigrationsError,strandedFileDataErrorserver/storage/boot-check.jsserver/db/boot-check.jsThe no-op test helper
__resetStorageForTestsis removed, and with ituninstallFacadeStorage()intests/pg/helpers/harness.js: a fork pg test drops that call,closeTestDb()is the whole teardown. Retention sweeps refuse any scope that names an organization. A fork that calledreplaceTagLinksorgetUserIdByEmailwith anorgIdpasses that organization’s scope instead. The storage-scope reference explains the contract.This release adds no database migration or new configuration variable. See the 1.67.0 release on GitHub for the commit list.
-
v1.66.0
Enforced SSO now also blocks password and magic-link sign-in through the API. Rate-limit headers report the real request budget. Forks must update 42 storage calls and two rate-limit helpers.
Enforced SSO closes the local sign-in endpoints
With
SSO_ENFORCE=trueand a valid SSO configuration, the login forms were already hidden, but password login, magic-link and password-reset requests still worked when called directly. These endpoints now refuse with HTTP 403 and{ "ok": false, "error": "sso_required" }before any credential check, token or email. Magic links and reset links issued before enforcement are refused too. Existing sessions keep working, and a disabled or incomplete SSO configuration does not activate the guard.Rate-limit headers show the real request budget
X-RateLimit-*headers on public API responses used to advertise a theoretical daily total, while the request limiter refused much earlier in a burst with a fixedRetry-After: 60. Request responses and request 429s now report the limit, remaining requests, reset time and retry delay from the same limiter decision. The daily AI and export quota headers keep their meaning. If a client reads these headers, expect much smaller numbers that are actually reliable.Breaking changes for forks
Two rate-limit helpers change.
getRateLimitHeadersinserver/storage/api-usage.jsaccepts only the explicitaiorexportslimits; request callers usecheckRequestRateLimitfromserver/utils/rate-limit.js.allowRequestRedisinserver/utils/rate-limit-redis.jsbecomesallowRequestRedisDetailsand returns a decision object: read its.allowed, or useallowRequestfromserver/utils/rate-limit.jsfor a boolean.The following 42 analytics, live-session, interaction-slide and share-link exports now take
StorageScopeas their first argument. Insert it before every previous argument, including the database argument ingetCompletionCounts(scope, db, ...)andgetValidShareLinkById(scope, db, ...). Import paths are unchanged; the old signatures are removed.Module Exports receiving a new first scopeargumentserver/storage/analytics/aggregations.jsgetCompletionCounts,getDetailedSlideEngagement,getInteractionHeatmapData,getPresentationAnalyticsOverview,getViewerJourneyDataserver/storage/analytics/dashboard.jsgetDashboardSummary,getDashboardTimeline,getPresentationsWithAnalytics,getSourceBreakdown,getTopPresentationsserver/storage/analytics/slide-views.jsdeleteOldSlideViews,endAllSlideViewsForSession,transitionToSlideserver/storage/analytics/view-sessions-gdpr.jsanonymizeOldIpAddresses,deleteUserAnalyticsData,eraseAnalyticsDataForDevice,eraseAnalyticsDataForSession,exportUserAnalyticsDataserver/storage/analytics/view-sessions.jscreateViewSession,deleteOldViewSessions,getActiveViewerCount,getViewSessionsForPresentationserver/storage/analytics/weekly-summary.jsgetTeamWeeklyAnalytics,getWeeklyAnalyticsForUserserver/storage/interaction-slides.jsensureInteractionSlide,getInteractionSlide,updateInteractionSlideserver/storage/live-sessions/close.jscloseSessionserver/storage/live-sessions/db.jscountPersistedSessions,deletePersistedSession,hydrateSession,hydrateSessionsForPresentation,persistSession,schedulePersist,sweepExpiredSessionsserver/storage/live-sessions/sse.jstouchSessionSyncserver/storage/share-links/access-log.jsgetShareLinkAccessLog,logShareLinkAccessserver/storage/share-links/crud.jsgetValidShareLinkById,shareLinkCreatorNamesserver/storage/share-links/guests.jsrequestGuestVerificationserver/storage/share-links/index.jscleanupExpiredShareLinksPass the organization scope of the presentation involved. Authenticated GDPR subject requests, token-derived anonymous erasure and retention jobs need an explicit instance scope with a reason. A scope does not replace caller authorization. The storage-scope reference explains the contract.
This release adds no database migration or new configuration variable. See the 1.66.0 release on GitHub for the commit list.
-
v1.65.0
Invalid slide fields are caught on every save path, with inline recovery in the editor. Forks must update 35 storage calls to pass their scope first.
Correct invalid fields before saving
Slide changes now use the same field validation in the editor, API and MCP. The editor points to the field and language that needs attention and keeps your edits available while you correct it. Existing invalid values on unchanged slides remain readable; changing such a slide asks you to repair them. Incomplete translations can still be saved without copying source text into the translation.
Breaking change for fork storage calls
The following 35 exports now take
StorageScopeas their first argument. Insert it before every previous argument, including the database argument inensureMembership(scope, db, ...). Import paths are unchanged; the old signatures are removed.Module Exports receiving a new first scopeargumentserver/storage/collaborators.jsaddCollaborator,getCollaboratorPermission,listCollaborators,removeCollaborator,updateCollaboratorPermissionserver/storage/display-identity.jsresolveDisplayNames,resolveNamesForAddressesserver/storage/identity-resolver.jsresolveIdentity,resolveIdentityByEmailserver/storage/identity-verification.jsverifyIdentityConsistencyserver/storage/identity.jsgetUserByEmailGlobal,resolveActiveMembership,resolveActiveOrganizationserver/storage/login-membership.jsensureMembershipserver/storage/password-reset.jscleanupOldAuditLogs,getPasswordChangedAt,hasDatabaseCredentials,logAuthEvent,verifyUserPasswordserver/storage/user-organizations/memberships.jsaddMember,countOrganizationMembers,getMembership,getMembershipByEmail,getOrganizationMember,listOrganizationMembers,listUserOrganizations,removeMember,transferOwnership,updateMemberDesigner,updateMemberRoleserver/storage/user-organizations/organizations.jscreateOrganization,deleteOrganization,getOrganizationById,getOrganizationBySlug,updateOrganizationUse the target organization scope for organization-bound mutations and the parent deck’s organization for collaborator operations. Pre-authentication, provisioning and instance-wide identity or audit operations need an explicit instance scope with a reason. A scope does not replace caller authorization. The storage-scope reference explains the contract.
This release adds no database migration or configuration requirement. See the 1.65.0 release on GitHub for the commit list.
-
v1.64.0
App styles now live with the features that own them. Forks importing or patching core CSS must update paths and check cascade order.
App stylesheet sources now follow their owning feature or shared UI widget. Forks that import or patch these files must update the paths before upgrading; the old paths have no compatibility shims.
CSS path changes
All paths below are relative to
client/styles/app/.Old path New path shell/20-editor-layout.csseditor/20-editor-layout.cssshell/21-editor-skeleton.csseditor/21-editor-skeleton.cssshell/107-collab-presence.csseditor/107-collab-presence.cssshell/05-avatar.cssuser/05-avatar.cssshell/99-notification-bell.cssuser/99-notification-bell.cssshell/96-activity-feed.csslist/96-activity-feed.csslist/17-deck-grid.csseditor/17-deck-grid.csssettings/103-data-source.csseditor/103-data-source.csseditor/modals/10-modals-base.csscomponents/10-modals-base.csseditor/modals/11-modals-loading.csscomponents/11-modals-loading.csseditor/106-shortcuts-overlay.csscomponents/106-shortcuts-overlay.csscomponents.csscomponents/00-components.csseditor/modals/13-modals-misc.csstheme-select/13-theme-picker.cssshell/110-sandbox-banner.csslist/110-sandbox-examples.cssshare-viewer/75-share-viewer.csseditor/modals/19-modals-revoke.cssOnly the theme-picker section moves out of
13-modals-misc.css, the example shelf out of110-sandbox-banner.css, and the revocation dialog out of75-share-viewer.css. Their other styles remain with the original feature. Slide-picker and translation variants from the modal base live ineditor/modals/10-modals-editor.css.components.cssis now a generated aggregator.Check source-order overrides
Shared modal bases now load before all feature styles, and editor layout follows shell utilities. Selectors and declaration values are preserved, but forks whose overrides rely on source order should check the cascade.
This release adds no database migration or configuration requirement. See the 1.64.0 release on GitHub for the commit list.
-
v1.63.0
Slide-lock policies now require an explicit organization scope. Forks calling storage helpers need two updates.
This release tightens the storage boundary used when saving slides. Slide locks keep their existing behavior, while the lock-policy functions validate the organization scope before proceeding. Forks that call these helpers directly must update their calls before upgrading.
Slide-lock helper calls
In
server/storage/presentations/crud/enforce-slide-locks.js, bothenforceSlideLocksandenforceSlideWritePolicynow take(scope, opts)instead of({ctx, ...opts}). Pass the organization scope as the first argument and removeopts.ctx. Their import path stays the same.Theme seed helper for forks
upsertThemeSeeds(db, seeds)moved fromserver/utils/theme-seeds.jstoserver/storage/themes.jsasupsertThemeSeeds(scope, seeds). Update direct imports and pass an explicit instance scope instead of a database handle. There is no compatibility export at the old path.This release adds no database migration or configuration requirement. For the commit list, see the 1.63.0 release on GitHub.
-
v1.62.0
Sandbox guests have tighter privacy boundaries, and organizations can set their own AI translation instructions.
This release closes more ways for visitors to a public sandbox to see or contact each other. Organization admins can also set terminology and target-language style for AI translations. Forks that use the former translation rules package or import presenter runtime files directly need to update before deploying.
If your fork uses
ciiic-translation-rules, save its terminology and per-language style text before upgrading. After upgrading, enter that text in Settings > Admin > Translation instructions before translating again. The package is no longer loaded and there is no automatic migration.Sandbox guest privacy
- Organization tags are hidden. The organization tag list and autocomplete no longer expose a shared vocabulary to sandbox guests. Guests can still use tags on their own private decks.
- Example decks cannot carry guest conversations. Curated examples remain readable, but their comments, counts, live updates and recent-comment feed are closed, including comments left before this release.
- Sharing controls are closed. Share links are refused in sandbox mode, and the Share dialog explains why its Guests controls are disabled.
Translation instructions for organizations
Admins can enter shared terminology and a separate style for each enabled target language under Settings > Admin > Translation instructions (
/settings#admin). The fields are stored inorganizations.settings.translationInstructions:terminologyapplies to every target language, whilestyleByLanguage[languageCode]applies only to that language. These instructions are used by AI translations when AI is enabled.To migrate a fork using
ciiic-translation-rules, keep a copy of its terminology and per-language style text before upgrading. After upgrading, transfer that text into the new fields, then remove the obsolete package and its Dockerfile installation. Check the saved instructions before using translation again; nothing is imported automatically.Import changes for forks
The presenter runtime files moved without compatibility shims. Update direct imports before syncing to this version:
Old path New path client/views/presenter/step.jsclient/lib/slide-runtime/step.jsclient/views/presenter/auto-advance.jsclient/lib/slide-runtime/auto-advance.jsFor the commit list, see the 1.62.0 release on GitHub.
-
v1.61.0
Forks importing the tag editor must switch to its new feature entry point.
The tag editor now lives with the tags feature. Its behavior is unchanged in Deckyard, but forks that import it by path need to update that import before upgrading.
For forks: tag editor import moved
Replace
client/views/list/tag-editor.jswith the public entry pointclient/views/tags/index.jsforcreateTagEditor. The old path has been removed without a compatibility shim. No stored-data migration or new configuration is required.For the commit list, see the 1.61.0 release on GitHub.
-
v1.60.0
Published deck descriptions follow the selected language; forks must update comments imports.
Published decks now use the selected language for their default description, untitled fallback and reading-view link. That includes search metadata and structured data, so an English deck no longer gets a Dutch default description. Empty lists and pickers also use a consistent appearance.
For forks: comments imports moved
Update custom imports before upgrading. Seven modules moved from
client/views/editor/toclient/views/comments/, with no compatibility shims:Previous path New path client/views/editor/comment-markers.jsclient/views/comments/comment-markers.jsclient/views/editor/comments-api.jsclient/views/comments/comments-api.jsclient/views/editor/comments-panel.jsclient/views/comments/comments-panel.jsclient/views/editor/comments-panel-actions.jsclient/views/comments/comments-panel-actions.jsclient/views/editor/comments-panel-renderers.jsclient/views/comments/comments-panel-renderers.jsclient/views/editor/comments-panel-sse.jsclient/views/comments/comments-panel-sse.jsclient/views/editor/comments-read-state.jsclient/views/comments/comments-read-state.jsImport
createCommentMarkers,createCommentsApiandcreateCommentsPanelthrough the publicclient/views/comments/index.jsentry point. The other moved files are internal feature concerns. This release requires no new configuration or stored-data migration.For the commit list, see the 1.60.0 release on GitHub.
-
v1.59.1
Library items with malformed language versions are now refused before storage.
Deckyard now checks the shape of a library item’s language versions before saving it. Invalid data gets a clear refusal instead of being stored and potentially losing language content during a later edit. Existing valid library items continue to work as before.
The release also restores the QR renderer used by the website’s automated documentation captures. It does not change the QR codes shown to people using Deckyard.
For the commit list, see the 1.59.1 release on GitHub.
-
v1.59.0
Client modules now live with the views and shared widgets that own them. Forks importing the moved files must update their paths.
Forks with direct imports from Deckyard’s client modules must update the paths below before upgrading. This breaking change ships as a MINOR release while Deckyard is in beta. Decks and stored data need no migration.
The editor’s live-edit modules now live with the editor, and shared browser helpers live in the DOM, network and state layers. This gives each module one clear owner. There are no new features or required environment variables in this release.
Import changes for forks
Old path New path client/lib/util/clipboard.jsclient/lib/dom/clipboard.jsclient/lib/format/lang-selector.jsclient/lib/dom/lang-selector.jsclient/lib/util/qr.jsclient/lib/dom/qr.jsclient/views/ui-mode-switcher.jsclient/lib/dom/ui-mode-switcher.jsclient/lib/util/file.jsclient/lib/net/file.jsclient/lib/util/debug.jsclient/lib/state/debug.jsclient/lib/util/dev-runtime.jsclient/lib/state/dev-runtime.jsclient/lib/util/id.jsclient/lib/state/id.jsclient/lib/collab/live-doc-binder.jsclient/views/editor/live-edits/live-doc-binder.jsclient/lib/collab/presence-session.jsclient/views/editor/live-edits/presence-session.jsclient/lib/collab/textarea-merge.jsclient/views/editor/live-edits/textarea-merge.jsclient/views/notes-join.jsclient/views/notes/notes-join.jsclient/views/present-window.jsclient/views/presenter/present-window.jsclient/views/theme-select.jsclient/views/theme-select/index.jsThe old paths have no compatibility shims. Update any fork imports to the new paths before syncing to this tag. The complete change is in the 1.59.0 release on GitHub.
-
v1.58.0
Guests can comment through share links again. Image previews show the whole picture, chart titles follow the theme, and weekly digest emails work for accounts that opt in.
Forks importing the old QR or slug helpers must update their imports before upgrading. This is a MINOR release during beta because those module paths changed.
This release fixes guest comments, makes image previews and chart headings more consistent, and restores the weekly digest as an opt-in email. AI and MCP now use the same slide-type offer across more paths, including custom types in stdio sessions.
Sharing and editing
- Guest comments work through share links. Guests can read comments and add, edit or delete their own through the shared deck’s link.
- Image previews show the whole selected picture, including portraits and square images. Chart headings now follow the theme’s heading capitalization setting.
- Invalid publication slugs are refused inline. An explicitly supplied URL slug must already be valid; it is no longer silently repaired. Slugs derived from titles handle accents and separators consistently. Existing published slugs stay as they are.
- Form refusals stay next to the field. Wrong passwords and data-source refusals appear inline; an existing user address marks and focuses the email field. Settings and its sidebar use the same tab list.
AI and agents
- MCP stdio sees published custom slide types and the default organization’s curation. A failed lookup reports a refusal instead of offering an empty catalogue. AI generation accepts a type only where that type is offered.
- Imports and AI requests agree on their inputs. The v1 API refuses a non-string
rawvalue, safe creation settings are carried through, and ordinary imports use the same placeholders for empty image fields as AI output. - HTTP failures carry machine-readable codes. Top-level application errors retain their code and registered details; MCP transport failures use
errorandmessage. JSON-RPC errors keep their protocol shape.
Breaking changes for forks
- QR helpers move from
client/lib/slide-runtime/poll.jstoclient/lib/util/qr.js. Update imports ofrenderQrToCanvasandcreateQrDataUrl. The unusedgetOrCreateVoterIdandmountPollBarsexports are removed. server/utils/slug.jsand itssafeSlugexport are removed. UsegenerateSlugfromshared/slug.js; pass{ fallback: 'presentation' }where that default is needed. Explicit slugs are checked withisValidSlug, rather than repaired.
For self-hosters and forks
- Weekly digest emails work again and are opt-in. Accounts without an explicit enabled preference receive no digest. An account with
digest.enabled: truecan receive mail again; review existing saved preferences if you previously enabled the digest. - Migration 091 normalizes stored deck email stamps. It trims and lowercases
owner_email,created_byandupdated_by, turns blank values into NULL, and logs the affected row count for each column. New writes use the same normalization. Back up the database before upgrading; the migration does not restore the original casing on rollback. - AI conversation logging has one optional setting:
AI_SESSION_LOGS. When unset, full conversations are logged only withNODE_ENV=development; production logging is off. Set it explicitly to enable or disable it. Fork callers must stop passing the removed per-callenableLoggingoption. - Slide-type offer loaders require a validated storage scope. Fork integrations calling those loaders should pass that scope. There are no dependency changes or new required environment variables; the core slide-type count remains 34.
For the complete, commit-level list, see the 1.58.0 release on GitHub.
-
v1.57.0
AI and agents build with your organization's own slide types, and the Notion import works again. The PPTX template moves to the theme card and opens on a sample per layout, the slide library gets its own address, deck search reads translations, and a round of feedback fixes: fewer toasts, the server's reason when something fails, and focus that lands where you were.
The thread through this release is that the AI paths finally see the same slide types you do. A deck the model generates, imports or analyses is composed from your organization’s own types and limits, not from the built-in catalogue. Around that: the PPTX template finds a better home, the slide library gets an address you can share, and a long list of small things the interface told you badly now get told right.
AI and agents use your organization’s slide types
- Generated and imported decks use your own types. Create over MCP, convert, Notion, markdown import, the wizard, append and refine all compose from the organization’s type map and its configured defaults, and the AI is offered your custom types while disabled ones are left out. The analysis that proposes a type does the same.
- The Notion import works again. Every Notion import with content to refine had failed since 1.0.0; it runs through again.
- AI suggestions and the v1 API insert into the right group. A slide inserted after a child slide now joins that child’s group instead of landing at the top level between the children.
- Translate and fill-missing reach every string in a list or card item, including ones the type does not declare, so a translation gap can finally close.
- The AI only picks a background its slide type offers, so a slide no longer comes back with a choice the editor silently resets.
Editing
- The slide library has its own address,
/app/slide-library. A copied slide link opens that slide in a fresh tab, and Close takes you back to the library; a missing or hidden slide says so inline. Before, the library route was treated as a deck called “slide-library”. - Deck search reads translations and item titles. Deep search finds slide text in other language versions and in the titles of list and card items.
- A chart binding takes a range and gets a table. Bind a chart to
A1:B10in a sheet or CSV and the whole table comes in with the first row as header, where before one value arrived and the chart broke. - Settings keeps its tab without filling your browser history: switching tabs no longer adds an entry per click.
- Bunny video players take their colours from the theme instead of fixed brand colours, and a bare Bunny video UUID now needs a library: fill in the slide’s Bunny library ID, paste a full Bunny URL, or set
BUNNY_LIBRARY_IDon the instance so new video slides start in your own library.
Feedback that says what happened
- Twenty-eight success toasts are gone where the screen already shows the result: deleting a theme, duplicating a deck, uploading a photo, creating a collection. Toasts stay for saves with no other visible sign and for effects that happen off screen.
- A failed action shows the server’s reason, “too large” or “wrong type”, instead of generic copy, for image uploads, organization switching, bulk trash and restore, PDF export and more.
- A background failure stays where it belongs. A failing export poll, slide-type reload or autosave of a library description shows an inline message next to the thing that failed, and clears on the next success, instead of a toast that disappears in five seconds.
- The minute-long “Saving changes…” toast is gone. A small pill bottom-left appears only when a save takes longer than a moment, and an image being uploaded dims in place.
- A failed job says why in plain words, “Presentation not found”, or the panel’s own wording, never a raw internal error.
- A row that disappears hands focus back to its list. Deleting a theme, font, user, member, collection, API key or trashed deck moves focus to the neighbouring row and announces it; restoring from the trash lands on the next card.
Presenting and viewing
- KPI tiles are readable on every dark ground. The tile glass follows its ground and the positive and negative deltas clear AA contrast on Midnight, calm and any custom dark theme.
- The chart error card speaks the deck’s language and sits on the slide’s ground, where it used to be a bright plate on a dark slide, always in Dutch. Line charts now offer “Show values” like the others.
- Analytics: the views timeline labels each gridline with its own value, in whole steps, and screen-reader texts on the heatmap and timeline get their plural right.
- Link previews follow the instance. With
OG_IMAGE_URLandOG_DESCRIPTIONset, a shared app link unfurls with your own name, image and description, and a share or published link without a deck image falls back to that image.
Exporting and embedding
- The PPTX template is downloaded from the theme, not from a deck. Each theme card in Settings → Themes has “Download PPTX template”, named after the theme. It opens on one sample slide per layout, the theme name on the cover and a dashed picture frame, instead of an empty black slide.
- Video placeholders in a PPTX export take the theme’s colours, not fixed brand colours.
- The embed SDK box grows by the strip under
ui: 'strip', so the controls are no longer cut off, and the controller getssetOptions(patch).
For self-hosters and forks
No database migrations. Three new variables, all optional:
OG_IMAGE_URLandOG_DESCRIPTIONfor link previews,BUNNY_LIBRARY_IDfor new video slides. The slide-type count stays at 34.- The deck schema goes from v18 to v22, through four read-funnel steps that run on read, write and import:
slidecreator.deckfolds todeckyard.deck; image-slidelayoutfolds intofitandbleed;altNlandaltEnfold intoalt; the legacybgImage,bgAltandbgImagekitFileIdon any slide fold into the shared background layer. JSON schema ids move to/v22/. - The import drops content keys a type does not declare, on JSON,
.deck, Notion, convert and the AI paths. A fork type that reads an undeclared key must declare it, as a hidden field or throughinstanceKeys. - MCP tightens up.
add_slideandupdate_slidestore content as sent or refuse it with400anderrors; they no longer repair it silently (validation: 'fix'stays). Theidalias is gone: every deck-scoped tool takespresentationId.get_slide_typesreturns the type’s real sample asexample. A deck withoutlangdefaults tonlon convert, iterate and append. - API: a refused action on a promoted question answers
closed(409);lockednow means only the slide lock (423).GET /api/jobs/:idno longer returnserrorfor a failed job, onlymessagewhen there is a sentence for the caller. NewGET /api/v1/themes/{id}/template.pptx; the per-deck template route is removed. - Seams a fork touches:
newSlide(),convertSlideToType(),newPresentation()and the AI fix and refine functions requireslideTypesand throw without it;loadSlideTypeContextis replaced byloadSlideTypeOffer(); a custom type’sai.examplesentries take the form{ variation, content }; the title-view seam incore-layouts.jsexportstitleInlineEditand renders without a ground whenview.backgroundisnull. Removed:validate-slide-structure.js,shared/phosphor-icons.js,shared/slide-schemas.js,TitleView.bgImage,resolveSlideBgImage,BUNNY_PLAYER_COLORSand the?s=alias for?slideId=. - A global
box-sizing: border-boxreset now lives in the shared primitives and on.slide; fork CSS written for content-box sizing can shift by its padding. The save indicator class is.editor-save-statuswith.is-failure.
For the complete, commit-level list, see the 1.57.0 release on GitHub.
-
v1.56.0
Embed a deck with a slim controls strip below the slide instead of arrows on top of it. New title slides open on a theme photo, viewer pages speak the deck's language, counted words get their plural right, and text over a background photo stays readable however the deck was made.
The one new thing in this release is for anyone who puts a deck on a website. The rest is a round of fixes you notice while editing and presenting: a deck that looks right from the first slide, pages that speak the right language, and one less way for text to disappear into a photo.
A controls strip for embedded decks
- Add
?ui=stripto an exported deck (/p/or the downloaded.html) or to a hosted embed and you get the slide edge to edge with one slim bar below it: Previous, Next, the counter and Fullscreen. A site that iframes a deck no longer has to lay its own arrows over the slide. - In fullscreen the strip steps aside and comes back when you move the mouse, just like in the presenter.
- The regular export bar now has a Fullscreen button too, and the counter in the embed is readable again (it was black on black).
Editing
- A new title slide opens on a photo from the theme when the theme has background presets, whether you insert it, import it or convert to it. The Background image section is open by default on the cover. Themes without presets stay plain.
- Text over a background photo stays readable on every route. A deck that never passes through the editor (made over MCP, imported, generated by AI or presented straight away) now gets the same text colour and scrim the editor would pick, in the presenter, on a share link and in the PDF.
- A theme switch that changes a slide’s type now does so in every language version of the deck, each with its own text, instead of only in the main language.
- The slide toolbar shares the grey of the canvas, so you can tell at a glance which bar belongs to the slide and which to the whole deck.
Viewing and the interface
- Share links, follow-along pages and
/gospeak the deck’s language, then the browser’s, then English. A saved preference or?locale=still wins, and/gois now translated. - Counted words get their plural right: “1 slide”, not “1 slides”, in the presenter start screen, the library, sharing, importing and dozens of other counters.
- Small sandbox-round fixes: the Analytics button appears right after you create a share link, the library language filter counts correctly, Home in Dutch is fully Dutch, the Midnight theme card is readable and the presenter top bar fits on a phone.
For self-hosters and forks
No database migrations and no new variables. The slide-type count stays at 34.
- An MCP tool that fails unexpectedly now answers “Internal server error” and logs the rest, like the v1 API. A custom tool that throws a plain
Errorwith a message meant for the user should throw anAppErrorwith the right status instead. - The default interface language is English everywhere. A fresh browser without a preference sees the login page in English;
DEFAULT_UI_LOCALElives inshared/constants/ui-locale.js. - Counted strings use one plural seam: a
t()call for a counted phrase passes{ one, many }as its fallback plus{ count }, and a fork locale fills<key>.oneand<key>.many. - Background-image contrast is settled on the server write seam. A custom write path through the storage facade gets it for free; one that bypasses
updatePresentationRowdoes not. - A custom embed toolbar override must keep
data-presenter-chrome, or it will not stay put under the mouse in fullscreen.
For the complete, commit-level list, see the 1.56.0 release on GitHub.
- Add
-
v1.55.0
Translated decks stop losing text: theme changes, translations and MCP edits keep every language version. The editor, the API and MCP now refuse the same bad input with the same answer, Insights shows real completion rates, and the sandbox got a round of polish.
This release is about one deck behaving the same however you touch it: in the editor, through the public API or through an AI agent over MCP. Most of the work is underneath, but it shows up as fewer silent surprises. If you work with translated decks, update: several writes could overwrite another language version of a deck with the source text.
Check before you update: API and MCP changes
Deckyard is in beta, so stricter input checks ship in a minor release. If you call the API or run an MCP agent, these answers changed:
- Unknown values are refused instead of guessed. A comment filter (
status,commentType,since), a theme,ownership,viewOnly,deep, an analyticssortorperiod, a non-booleankeepAsCollaboratororisViewOnly, and a missing or unknown translation language are all a 400invalidwith the field indetails.field. - Too big is a 409
limit_exceeded, everywhere. A v1 save, a create, a translation or an MCP write over the size limit used to answer 200 or 201 without saving; now it says so. - MCP
list_presentationsandlist_recent_commentsknowcollection(your decks plus the organisation-visible ones, as Home shows them).allnow includes organisation-visible decks, andownedmatches your account instead of an e-mail string. - MCP tool errors carry the v1 envelope (
error,message,details), so an agent sees which field was wrong. A failed AI generation over v1 is a 502bad_gatewaythat says what the model did wrong, and every v1 error carries the rate-limit headers. - Ownership and visibility changes are stricter: changing visibility needs read access to the deck, an org admin only changes decks they can read, and the notification and webhook links are built from
APP_URLinstead of the request’s Host header.
Translated decks keep their languages
- A theme change or a publish on a deck you last edited in another language no longer overwrites that language version.
- Translating from a non-dominant language (say German on a Dutch deck) no longer overwrites that source version, and the editor, the API and the translation queue now refuse the same mistakes.
- MCP edits (
update_slide,remove_slide,reorder_slides,append_slides,compress_presentation,iterate_presentation) write only the slides and leave the other language versions alone.remove_sliderefuses the last slide, and generated slides with an unknown type are refused instead of stored. - A deck started from library slides gets the language those slides are written in.
Insights
- Completion rate shows a real number on Insights, in Top Performing and on a deck’s own analytics: the share of sessions that reached the last slide, instead of always a dash.
Editing and the sandbox
- A theme change that cannot convert a slide is refused, with the reason next to Apply, instead of leaving the slide unchanged under the new theme.
- The Building blocks on Home show the same slide preview as the library, also on a phone, and the “New to you” badge no longer covers the type label.
- The sandbox is more honest about being a sandbox: no pointless Sign out, the account tab explains why photo and password are missing, the slide language and library previews follow the interface language, and opening an example deck fills Insights with its viewing history.
- Analytics pages no longer show literal braces (
Device {id}), on any installation.
For self-hosters
No database migrations and no new variables. The slide-type count stays at 34.
For the complete, commit-level list, see the 1.55.0 release on GitHub.
- Unknown values are refused instead of guessed. A comment filter (
-
v1.54.0
PowerPoint export you can edit: most slide types now arrive as real text, pictures, charts and tables instead of images. Text styles are offered per slide type, images upload in batches, and the sandbox library shows real example slides. Forks and API clients should read the breaking changes.
The headline of this release is a second PowerPoint export: one you can keep editing. Next to that, text styles are now offered per slide type instead of everywhere, images upload in batches with optional AI alt text, and live presenting, stock media and the public API can each be left out of an installation. Deckyard is in beta, so the breaking changes below ship in a minor release; read the first section if you run a fork or call the API.
Check before you update: API and fork changes
- The
custom-html-slidetype is gone (34 built-in types, was 35). A stored slide of that type shows as an archived slide with its markup as text.CUSTOM_HTML_EDITOR_EMAILSdoes nothing anymore and can leave your environment. - Text styles are an offer per type. Alignment and size are only accepted where the slide type declares them; anything else is refused with a 400
text_style_*error. Per-field text colour is gone and stored colours drop on read, and themes losetextSwatches. A fork type with its own JavaScript declares its offer (textStyle,itemTextStyle,textStyleSets) or its stored styles drop. The full rule is in the text-styles reference. - Quote slides keep every quote in
quotes[]. API and MCP callers postquotes[]; the flat keys are refused by strict validation. Stored decks fold on read. - A fork type’s CSS lives only in
custom/styles/. Acustom/slide-types/*.jstype whose rendering emits a<style>block is refused at load; move the rules to a stylesheet nested under the type’s root class. A fork can now compose core’s title slide throughshared/slide-types/core-layouts.jsinstead of copying it. - The ImageKit slot passes
createImageKitPaneland returns{ el, detach }; the picker owns the dialog. PassingopenImageKitthrows. - Client modules moved for forks that import by path: slide library, comments, collections, user, the analytics tracker and the theme select are views now (
client/views/…/index.js). The full path list is in the GitHub release.
PowerPoint you can edit
- Two PowerPoint rows in the export menu. “PowerPoint” stays pixel-perfect: every slide as an image, videos play. “PowerPoint, editable” writes slides on your theme’s layouts as real text, and tells you which slides still went along as images.
- Most slide types export natively: title, text, list, table, text blocks and end slides, plus image, image with text, quote, chapter title, KPI cards, charts (as editable PowerPoint charts), callouts and comparisons. Bullets survive mixed formatting, also in LibreOffice.
- Your uploaded fonts keep their desktop name in the PowerPoint file, so a machine with the font installed shows it.
- On the API and MCP:
GET /api/v1/presentations/{id}/export/pptx-editable(headerX-Image-Slideslists the image slides), and MCPexport_presentationofferspptx-editablewith a download link.
Editing
- Text styles where they mean something. Alignment and size appear only on slide types that offer them. On the title slide, title and subtitle scale S/M/L independently. MCP clients see the offer per type in
get_slide_types. - Item settings have their own tab. Click a row, cell or card and its settings (colour, arrow, icon, link) sit in a “This row” / “This cell” / “This card” tab, so “All text” is no longer needed for them.
- Inspector choices pick their own control: short option lists become segmented buttons, longer ones a dropdown, with the explanation in a tooltip.
- The KPI note is editable on the canvas, and on quote slides the portraits of every quote can be picked on the canvas.
- The slide-type builder can mark the heading of a custom type, which the reader then shows as the slide’s title.
- Slide-type names in the picker are translated into Dutch where they were still English.
Images
- Upload a batch in one go for the library, galleries, logo walls and image sets, and edit names and alt text per language on one screen before confirming.
- Generate missing alt text for the whole batch, only after you agree to it (OpenAI, number of images, possible cost), and never over text you wrote yourself.
Presenting, AI and the sandbox
- The speaker-notes view always fits the current slide, with the next slide, notes and questions beside it on a wide screen.
- AI outlines are built in two steps, source analysis then slide structure, and Claude defaults to Opus 5. Generated text that runs too long is refused and rewritten once instead of being cut off with ”…”.
- The sandbox library shows real example slides and a collection, and the building blocks are on Home from the first visit.
Running in production
- Three more modules can be left out:
LIVE_ENABLED(follow-along, live sessions, polls),STOCK_MEDIA_ENABLED(gradients, Unsplash, Giphy) andPUBLIC_API_ENABLED(/api/v1, MCP and API keys). All default to on. - Deleting a user revokes their pending sign-in links.
Smaller fixes
“Workspace default” names the theme it resolves to; a poll without codes names the real follow URL; the playful theme’s accent pair reaches 4.5:1 contrast; the import progress bar moves steadily; comments and compression name a custom slide type by its own heading.
For self-hosters
One database migration, applied automatically at start. Three new optional variables (above), all on by default;
CUSTOM_HTML_EDITOR_EMAILScan go. The built-in slide-type count drops from 35 to 34, and the deck schema is now version 18.For the complete, commit-level list, see the 1.54.0 release on GitHub.
- The
-
v1.53.0
Choosing and uploading an image fits on one screen, with a direct upload button and optional automatic alt text. Installations without uploads no longer show broken upload buttons, and analytics can now be switched off entirely.
This release makes working with images quicker and lets an installation leave out what it does not use: uploads and analytics. Nothing needs to change when you update.
Images
- The image picker fits on one screen. The preview sits next to the form, less-used fields are behind “More details”, and the action bar stays at the bottom. “Use this image” also saves your changes.
- Upload straight from an image field. Next to “Choose image…” there is now an “Upload from computer” button.
- Alt text in one step. The AI button for alt text sits by the alt field, and a remembered switch, “Generate alt text automatically after upload” (off by default), turns uploading and writing alt text into one step.
- The Back button in the detail view no longer leaves the picker empty.
Installations without uploads
- No upload buttons where uploads are off. With
UPLOADS_ENABLED=false,IMAGEKIT_ONLYor demo mode, upload buttons, drop zones and upload hints are absent instead of broken, also in the theme editor, where the logo URL field stays. In the public sandbox they stay visible but greyed out, with one sentence explaining why.
Analytics can be switched off
- A new optional environment variable,
ANALYTICS_ENABLED(on by default). Switched off, the tracking and analytics APIs answer 404, published pages get no tracking script, share links and follow-along do not track, the Insights, Analytics and public report pages do not exist, the weekly digest mail is not sent, and every analytics button and setting disappears from the app. - Stored data stays. Retention cleanup keeps running, and one log line at startup says how many rows are still there. With the switch on, nothing changes.
For self-hosters
No database migration and nothing to rename;
ANALYTICS_ENABLEDis the only new variable. The built-in slide-type count is unchanged.For the complete, commit-level list, see the 1.53.0 release on GitHub.
-
v1.52.0
Two environment variables are renamed and switched-off modules now answer with a plain 404. A release for administrators and forks: nothing changes in the editor.
This release is mostly for whoever runs Deckyard. Every optional feature now declares its switch in one place, so a feature that is turned off is off everywhere at once: its routes, its row in the feature table and its MCP tool. Two environment variables got clearer names along the way.
Check before you update: renamed environment variables
Deckyard is in beta, so this ships in a minor release.
NOTION_FEATUREis nowNOTION_ENABLEDand switches the whole/api/notion/*module (off by default). An installation that only setNOTION_SECRETmust now also setNOTION_ENABLED=true.DISABLE_ANALYTICSis nowEXTERNAL_ANALYTICS_ENABLED, with the meaning flipped:truemeans on.- The old names keep working with a warning at startup, until the first release after 1 November 2026. Rename them before then.
Switched-off modules answer the same way
- With
LIVE_DATA_ENABLEDoff,/api/data-sources/*answers 404 (it was 403). - With
RSS_FEED_ENABLEDoff,/feed/*shows the normal 404 page instead of a plain-text 404.
For self-hosters
No database migration. Check your environment for
NOTION_FEATURE,NOTION_SECRETandDISABLE_ANALYTICSand rename as above. The built-in slide-type count is unchanged.For the complete, commit-level list, see the 1.52.0 release on GitHub.
-
v1.51.0
An instance-health view in the admin, comments that no longer get lost right after an edit, and one shared type ladder for cards and diagrams. Creating, duplicating and deleting decks and comments now runs through one service; API clients and forks should read the breaking changes.
This release gives administrators a view of how their instance is doing, fixes a way a comment could get lost in the editor, and makes cards and diagrams scale more consistently. Under the hood, decks and comments are now created, duplicated and deleted by one service, whichever way you reach Deckyard. That changes a few details for API clients and forks; read the first section if that is you.
Check before you update: API and fork changes
Deckyard is in beta, so these ship in a minor release.
POST /api/v1/presentations/:id/commentsanswers aslideIdthat names no slide of the deck with 404slide_not_found(it was 400), next to the existingparent_not_found.- MCP
add_commentandreply_to_commentnow send e-mail notifications and fire the comment webhook, like the REST API and the editor already did. They answer a missing deck with 404 and a missing comment right with 403. - Creating a deck with an unsupported
langis refused before any work is done. - For fork code:
maybeFireWebhook(repoRoot, origin, event)takes an origin string where it took the request, andnotifyCommentCreatedandnotifyMentionsAddedno longer take a request.notifyCommentCreatedInAppandroutes/api/presentations/comments-shared.jsare gone; import fromserver/services/comments.jsinstead.
Instance health
- A new admin view shows how your instance is used: a census and usage per axis, counted at the points where Deckyard already handles the work.
Editor and comments
- A comment placed right after a slide edit is no longer lost. The pending save is stored first, then the comment is posted.
- The comment field stays inside the panel.
- The slide number sits in a gutter beside the thumbnail, in the accent colour instead of red.
Slides
- Cards and diagrams share one type ladder. Card roles and the diagram families now size their text from the same scale, so matrix titles and card headings scale more consistently.
Running in production
npm run doctorwarns when an https instance runs withoutTRUST_PROXY; without it, all users share one rate-limit bucket. The uploads check now tests the real upload directory.
Smaller fixes
- The sign-in screens use
APP_NAMEin the browser tab title.
For self-hosters
One database migration (
089_instance_health). The Docker Compose setup runs it before start; a plain Node install runsnpm run db:migrate. The built-in slide-type count is unchanged.For the complete, commit-level list, see the 1.51.0 release on GitHub.
-
v1.50.0
A production profile: npm run doctor checks an install, the Docker image has a health check and is a third smaller. PNG export is more reliable. The export settings lost their PDF_ prefix; check your environment before you update.
This release makes Deckyard easier to run in production, and more reliable when it exports. One thing needs your attention before you update: five export settings were renamed.
Check before you update: export settings renamed
The export settings apply to PNG as well as PDF, so they lost their
PDF_prefix:Old New PDF_EXPORT_TIMEOUT_MSEXPORT_RENDER_TIMEOUT_MSPDF_EXPORT_IMAGE_COMPRESSIONEXPORT_IMAGE_COMPRESSIONPDF_EXPORT_IMAGE_MAX_PXEXPORT_IMAGE_MAX_PXPDF_EXPORT_IMAGE_QUALITYEXPORT_IMAGE_QUALITYPDF_EXPORT_IMAGE_RETINA_SCALEEXPORT_IMAGE_RETINA_SCALEThere is no alias for the old names. An instance that still sets one silently falls back to the default after the update. If a fork calls the functions directly:
pdfImageEmbedTransformis nowexportImageEmbedTransform, andpdfImageCompressionConfigis nowexportImageCompressionConfig.Running in production
npm run doctorchecks an installation and tells you what is missing or misconfigured.- The Docker image has a
HEALTHCHECK, so your orchestrator knows when Deckyard is up. - The Docker image is smaller. A two-stage build takes it from 2.0 GB to 1.34 GB.
- New documentation for the production profile, deploying to a PaaS, and back-ups.
Export
- PNG export is more reliable. It waits for the page to load instead of for the network to go quiet, so the occasional “Navigation timeout of 30000 ms” on image-heavy slides is gone. Images are embedded at display size, as the PDF export already did.
- Fork seam images only where they are drawn. An export inlines an image from
custom/styles/*.cssonly when the export actually draws it; a deck without fork slide types no longer carries one along. - Chrome shuts down cleanly after an export on macOS.
Smaller fixes
- The message for a refused sign-in has room between it and the card below it.
For contributors
Eight new capture recipes cover the slide editors and the editor family, so the oldest hand-made documentation screenshots can now be regenerated. A font download that gets no answer is retried, and fails
npm installif it still does not arrive.For self-hosters
No database migrations. The built-in slide-type count is unchanged. Rename the export settings above before or with the update.
For the complete, commit-level list, see the 1.50.0 release on GitHub.
-
v1.49.0
Setting up SSO is easier to get right: Deckyard checks the redirect URI, reads groups from a claim, asks for extra scopes and shows instance admins the claims of recent logins. Operators get a multi-organization runbook, and forks get clearer seams.
This release is mostly about single sign-on: getting it right the first time, and seeing what your identity provider actually sends when it is not.
Single sign-on
- Deckyard checks the OIDC redirect URI at startup. If the redirect URI does not match, the server log warns and names the URI it expected, so you can copy it into your identity provider.
- A first-login step. The first person to sign in through SSO is guided through a “Your first login” step instead of landing on an empty instance.
- Groups from a claim.
OIDC_GROUPS_CLAIMtells Deckyard which claim holds a user’s groups. - Extra scopes.
OIDC_EXTRA_SCOPESadds scopes on top ofopenid email profile. Some claims only arrive behind a scope of their own, such as ZITADEL’s organization claim. - See the claims without devtools. Instance admins find the claims of the last ten SSO logins under Settings → Admin, refused logins included. They are kept in memory only, and tokens are never shown.
Multiple organizations
- An operator runbook and
npm run org:create. Running several organizations on one instance now has a written procedure, and a command to create an organization.
Forks
- A fork declares which seams it owns.
npm run fork:seamslists them, and the repository’sCLAUDE.mdis stable, so a fork can replace it with its own. - Images in fork styles reach the export. Local image
url()s incustom/styles/*.cssare now included in PDF, PNG and standalone HTML exports, the way fork fonts already were.
For self-hosters
No database migrations. The built-in slide-type count is unchanged.
Breaking, but only if you import client modules. Client module paths moved without compatibility shims:
client/lib/dom.jsis nowclient/lib/dom/index.js, and view entries live atclient/views/<view>/index.js. An instance without its own client code notices nothing. During the beta, breaking changes like this ship in a 1.x release.For the complete, commit-level list, see the 1.49.0 release on GitHub.
-
v1.48.0
Multi-organization installs can keep organization creation to instance admins, and a magic-link login no longer loops back to the login page. Export survives any unreadable font, and the pyramid slide reads on every theme.
A release of loose ends: two for multi-organization installs, one for export, one for the pyramid slide, and a tidier public API.
Multi-organization
- Organization creation can be restricted to instance admins. A new setting,
MULTI_ORG_USER_CREATE_ENABLED, defaults totrue, so nothing changes unless you set it. Set it tofalseand a regular user gets a 403 on “new organization”; only instance admins can create one. It only applies withMULTI_ORG_ENABLED. - A magic-link login no longer ends in a loop back to
/login. Someone who signs in through a magic link without an organization membership now gets one in the home organization: owner if it is empty, member otherwise. That is what SSO has done since 1.46.0. Single-organization installs are unchanged.
Export
- An unreadable font costs that font, not the export. 1.47.0 made export carry on past an unreadable font; this release does the same for every kind of font: uploaded, curated and local. The font that cannot be read is dropped with a warning in the server log, and the PDF or HTML export completes.
Slides
- The pyramid slide reads on lime and mist. Every level is now a visible surface, and the text follows the bar it sits on. On the lime and mist themes the top or bottom levels used to disappear into the background.
API
- The slide-type schema has one route. It lives at
/api/v1/schema/slide-types/:id.json, keyed by the published id (for exampleeu.deckyard.slide.title). The bare registry key and the second route,/api/v1/slide-types/{slideType}/schema, are gone. If you used either, switch over. - Every v1 operation has an
operationIdin/api/v1/openapi.yaml. If you generate client code from the spec, you now get stable method names.
For self-hosters
No database migrations and no breaking changes beyond the retired schema route above. The built-in slide-type count is unchanged.
For the complete, commit-level list, see the 1.48.0 release on GitHub.
- Organization creation can be restricted to instance admins. A new setting,
-
v1.47.0
Uploaded fonts are private: Deckyard serves them itself instead of from a public URL. Export no longer fails on one unreadable font and embeds every font role.
This release is about fonts. A font you upload no longer sits on a public URL, and exporting a deck is more robust about the fonts it embeds.
Uploaded fonts are private
- Deckyard serves uploaded fonts itself. A font you upload is no longer stored on a public bucket or
/uploads/URL. Deckyard serves it on/fonts/managed/…, only for this site and without indexing. For licensed fonts, the case this was built for, that is the boundary that can be reached: no open CDN or bucket. Exports (PDF, PNG, HTML) still contain the font, the way every PDF contains its fonts. - Using S3? Check the bucket policy. Public read should only apply to
uploads/*. If an uploaded font turns out to be publicly readable anyway, Deckyard refuses the upload with a message that says so.
Export
- One unreadable font no longer fails the export. If a font cannot be read, the text that uses it renders in its fallback and the export carries on.
- Every font role ends up in the export. A curated font in the mono or caption role (for example JetBrains Mono through
typography.mono) is now embedded in the PDF and PNG as well, not only the heading and body fonts.
For self-hosters
One database migration (
088, which dropsfont_variants.url) runs automatically on startup. If you had already uploaded fonts, runnode scripts/privatize-font-variants.js --applyonce afterwards to move them to private storage.Run it with
--checkfirst to see whether there is anything to move. The built-in slide-type count is unchanged.For the complete, commit-level list, see the 1.47.0 release on GitHub.
- Deckyard serves uploaded fonts itself. A font you upload is no longer stored on a public bucket or
-
v1.46.0
Themes become one kind of record that travels with a deck, SSO logins land in the right organization, the presenter console fits smaller screens, and the follow-invite slide gets a single QR plate.
A deck’s styling now goes wherever the deck goes. This release also routes single sign-on logins into the right organization, keeps the presenter console fully in view on smaller screens and simplifies the follow-invite slide.
Themes
- A theme is one kind of record. Built-in and custom themes are stored and handled the same way, and the styling of a deck travels with it on export and import, so a bundle opens looking the way it was made.
Organizations and single sign-on
- An OIDC login lands in the right organization. When your identity provider sends an organization claim, Deckyard uses it to put the user in the matching workspace.
- Users created through SSO get a membership straight away. An automatically provisioned user can work in a multi-organization instance from the first login, instead of arriving without a workspace.
Presenting and editing
- The presenter console fits smaller screens. On a narrower display the top bar wraps onto a second row, and the stage with the timer, the next slide and your notes stays fully in view.
- The follow-invite slide shows one QR plate instead of two cards, so the audience sees a single code to scan with the link beside it.
- Escape closes the topmost open surface first in the editor, rather than something underneath it.
Breaking, for forks
Deckyard stays on
1.xwhile the beta badge is up, so a minor release carrying a breaking change is the intended shape rather than a mistake. This one only touches forks that style the slide types against their own copy:- The follow-invite and feedback slides use new class names.
.sfi-cardand.sfi-card-titlebecome.sfi-qr-plate,.sfi-linkand.sfi-method-label, and the.sfi-qr-wrapwrapper is gone. Core themes move along with it; rename the rules in your own stylesheet.
For self-hosters
Three database migrations run automatically on startup:
085(theme seeds),086(presentation extensions) and087(organization_external_id). No manual step is needed. The built-in slide-type count is unchanged.For the complete, commit-level list, see the 1.46.0 release on GitHub.
-
v1.45.0
Edit is the main action on a deck card, editor feedback is quieter, the live-video overlay reaches the follow audience, and API v1 handles themes and slide types in one consistent way.
Opening a deck to edit it is now the obvious first step on the dashboard. This release also simplifies feedback in the editor, brings the live-video overlay to people who follow along, and makes the API stricter and more consistent about themes and slide types.
Dashboard and editor
- Edit is the primary action on a deck card. The other actions stay in the card, but Edit is the one that stands out.
- Save feedback is simpler. The editor reports saving in one place instead of several, and the selected slide’s thumbnail keeps its width.
- The slide-type builder labels its checkboxes properly. Clicking a label such as Required toggles its own checkbox and never another one.
Presenting and importing
- The live-video overlay reaches the follow audience. Viewers who follow a presentation on their own device now see the live video too, not only the main screen.
- The follow view respects the viewer’s analytics preference. The viewer’s own setting decides tracking in the browser; the owner’s opt-out is still enforced on the server.
- An imported bundle keeps its theme. If a bundle uses a theme that is not installed, the deck keeps that theme as its default instead of falling back to another one.
For API and MCP clients
- One form for a theme on v1. A deck names a library
theme, anddefaultis a value of its own. - Slide-type schemas resolve against canonical and organisation types. An org-specific slide type now validates on v1 the same way a built-in one does.
- Every create path checks the theme the way v1 does, and refuses a
contentByLangthat is not an object map.
No database migrations. The built-in slide-type count is unchanged.
For the complete, commit-level list, see the 1.45.0 release on GitHub.
-
v1.44.0
Video transcripts and matrix axis labels reach the reader, deck languages work consistently, and published decks get accessibility and privacy fixes.
Video transcripts and matrix axis labels now carry through to the reader. This release also makes deck languages more consistent and improves accessibility and privacy on published decks.
Readers and published decks
- Video slides bring their transcript into the reader. Matrix slides name their axes, and the reader shows those labels above the grid.
- Language choices match the deck. The presenter and follow view offer only the deck’s own languages, and the reader speaks in the deck’s language.
- Missing image descriptions are harder to miss. A published deck keeps warning while an image without alt text is live; images in custom markup go through the same alt check. Table headers in the canvas identify their column or row for assistive technology.
- Public pages protect the owner’s identity. They no longer derive an owner name from an email address, and published deck JSON-LD no longer names an author.
Editor and API
- The interface is more consistent. Icons use Lucide, Escape closes one open layer at a time, and the collaborator avatars fit in the topbar. A selected text field keeps its ring when focus moves to the sidebar. Logo walls scale to their logo count, and timeline cards scale to the slide canvas.
- API v1 reports real
createdAtandupdatedAtvalues. A slide-type change converts the slide or is refused; MCP then points the agent to the right route. Invalid-slide errors returndetailsas an object. - Language-version keys must be canonical. Deck and library writes reject aliases such as
enwhere the canonical key isen-GB.
For self-hosters and forks: database migrations
083_canonical_version_keys.jsand084_library_canonical_version_keys.jsconvert stored alias keys to canonical language keys. A deck with both an alias and its canonical key is left in place and logged; an operator must resolve it before that deck can be saved.The built-in slide-type count is unchanged.
For the complete, commit-level list, see the 1.44.0 release on GitHub.
-
v1.43.0
Click an image to select it and use Replace or Settings; logo walls scale with the slide, and the presenter language switch stays on one row.
Images are easier to work with in the editor. Click one to see a selection ring and a toolbar with Replace and Settings. Double-click still replaces it, and dropping a file onto a filled image now replaces it too. The old “Double-click to replace” labels no longer cover logos.
Logo walls display larger logos and scale them with the slide instead of the browser window. In the presenter, the language switch stays on one row even in a narrow topbar.
No database migrations or changes to the built-in slide-type count.
For the complete, commit-level list, see the 1.43.0 release on GitHub.
-
v1.42.0
The editor makes essential slide fields clearer, and the public API now requires theme and lang instead of themeId and language.
The editor now shows which fields a slide needs to look complete, without putting add-field buttons over your content. API clients need to check their deck requests:
themeIdandlanguagenow return a 400 error; usethemeandlanginstead.Clearer editing
- Add-field buttons stay out of your text. A button sits at the seam where its field would go; when there is no room, it becomes a small + in the margin.
- Essential fields prompt you to fill them in. An empty title on a title slide, image on an image slide or first card in a card list is visible without hovering. Optional fields appear on hover. These prompts are editor-only and do not appear in presentations, exports or print.
- Custom slide types can mark a field Essential in Settings → Slide Types. Required blocks publication without a value; Essential signals that the slide looks unfinished without one. Agents receive
essentialbesiderequiredin MCPget_slide_typesandGET /api/v1/slide-types/:type/schema.
Fixes and API clients
- The public API uses
themeandlangconsistently. Requests usingthemeIdorlanguagenow receive a 400 response naming the correct field. Responses report the actual theme and language, and aPUTwiththemechanges the theme. - An untitled blank presentation shows the error at its title field instead of silently refusing to save.
- Slide-jump buttons keep their destination after import, even when the destination slide receives a new ID.
No database migrations or changes to the built-in slide-type count.
For the complete, commit-level list, see the 1.42.0 release on GitHub.
-
v1.41.1
Copy to clipboard in the slide library now puts the slide on the editor clipboard, ready to paste into a deck.
Copy to clipboard in the slide library now works. Choose Use → Copy to clipboard, select a slide in a deck and press Ctrl/Cmd+V to paste the library slide there. This copies a slide within Deckyard; pasting it into another app is not supported yet.
No database migrations or changes to the built-in slide-type count.
For the complete, commit-level list, see the 1.41.1 release on GitHub.
-
v1.41.0
Image blocks can introduce a person with name, role and a short bio; countdown controls hide with the presenter bars in fullscreen; Duplicate copies a slide with its subslides.
The main addition is a person card: Image blocks now take a short description, so one block can show a photo, a name, a role and a bio. Nothing to do when you update.
Person cards on Image blocks
- Every block has an optional Description field (markdown) below the caption. Once a block has a description, the photo gets smaller and the card wider so the text fits. See Image Blocks.
- A block without a photo shows only its text in the presentation and the export; the empty photo placeholder is now only visible in the editor.
- A blank line is a blank line, in the description and in the text of Text blocks.
Fixes
- In fullscreen, the countdown controls hide together with the presenter bars and come back on a mouse move or Tab. The buttons are larger too.
- Duplicate in the slide menu copies the slide with its subslides, like the context menu and Cmd+D already did.
- Import errors appear next to the form. A JSON, Markdown or paste import that is refused (empty file, parse error, no file chosen) says so beside the field instead of in a toast.
- For forks and custom themes: the theme record now carries every field of core and fork themes without loss (brand, chart and background colours, alternative and payoff logos), and saving a theme with an unknown field is refused by name.
No database migrations between 1.40.0 and 1.41.0. The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.41.0 release on GitHub.
-
v1.40.0
A security fix for deck tags, your own SSO label and logo on the sign-in card, and every email, weekly digests included, in the recipient's own language.
This release closes a hole in how deck tags are checked, lets the sign-in card carry your install’s own SSO label and logo, and sends every email in the language its recipient chose. Update if more than one person signs in to your install.
Security
- Deck tags can only be read and changed by someone who may edit the deck. Until this version, the tag routes of a deck did not check the permissions on the deck itself.
Your own sign-in card
- The SSO button says what you want it to say. Set
SSO_BUTTON_LABEL, for exampleSign in with Acme ID, and the button and the invite report that points new members at it use those words. See Authentication. - Your logo on every sign-in card.
APP_LOGO_URLputs your logo above the sign-in card and in place of the Deckyard logo in the overview topbar. See Environment. - Both are optional; unset, the sign-in card looks as before.
Email in the recipient’s language
- Every email now follows the language its recipient chose in Preferences: invitations, guest mails, comment notifications and the weekly digests. Someone who never chose a language, such as a guest without an account, gets the install’s default email language.
- The digest setting in Preferences is respected. Someone who switched the digest off no longer gets it, and the weekday in the digest is right in every time zone.
Fixes
- Error messages in plain words instead of an upstream error. When ImageKit, Notion, Giphy, Unsplash or a data source provider fails, you now see one sentence saying what went wrong, not the service’s raw response.
- A custom slide type that is still in use cannot be deleted, and the message says how many decks use it.
- The theme tile on Home shows the theme’s own background.
- On an install without email, the notice after an invitation says how the invitee gets in, and stays until you close it.
- A revision conflict names who saved last. Useful for API and MCP clients that hit a 409.
- The trash hint states the server’s actual retention period.
- On the public sandbox, guests no longer share with each other and the interface says why; the sandbox banner states the real retention period.
No database migrations between 1.39.0 and 1.40.0; a normal upgrade is enough. The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.40.0 release on GitHub.
-
v1.39.0
Keep images out of the ImageKit picker with a tag, without deleting them, and no AI action left in the language switcher when AI is off.
A small release with one new setting for admins who connect an ImageKit archive. Nothing to do when you update.
ImageKit
- Hide images from the picker with a tag. Set
IMAGEKIT_HIDDEN_TAGSto one or more tags, comma-separated, and files carrying them no longer appear in the ImageKit picker, its search results or the tag suggestions. Slides that already use such an image keep working. Useful for an archive that marks duplicates or unsorted images instead of deleting them. Unset, nothing changes. See Media Services.
Fixes
- With AI switched off, the language switcher has no AI action either. It followed the other AI buttons that 1.38.2 already removed.
No database migrations between 1.38.2 and 1.39.0. The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.39.0 release on GitHub.
- Hide images from the picker with a tag. Set
-
v1.38.2
Growing a table works the way you expect, the canvas and the mail speak your language, AI buttons disappear when AI is off, and closing the window during an AI conversion or Notion import stops the work.
A patch with fixes only; nothing to do when you update. The one that started as a user report: adding a row to a table put the button in the wrong place.
Editing
- Growing a table works the way you expect. The + for a new row sat on the bottom edge of the slide instead of under the last row, and was called “Add item”. It now sits under the table, and columns grow along on the canvas.
- The canvas speaks the editor’s language. The placeholder fields (”+ Caption”) and the clear buttons on a Dutch canvas were in English; they now use the translated field name.
- With AI switched off, there are no AI buttons left. Convert in the slide menu, “Fill slide…” and the AI buttons beside each field disappear, instead of leading to an error.
- Countdown, video, KPI and team-card slides with server-rendered markup now work in the editor as well.
Mail in the recipient’s language
- Sign-in, password-reset and export-ready mail go out in the recipient’s language. A Dutch account used to get an English reset mail, and the export mail had no Dutch text at all (and an English sentence that said “1 presentation have been exported”).
- For admins: the email template settings show a language tab only for languages the server actually has, English and Dutch, instead of nine tabs of which seven quietly fell back to English.
Work stops when you walk away
- Closing the window during an AI conversion or a Notion import stops it on the server. Until this version the server carried on and created a presentation nobody would see, and used AI credits for it.
Libraries and search
- Searching tags finds what you type. A typed
%or_is a character, not a wildcard, and names such asİstanbulare found. - Saving in the library is all or nothing. A failed collection update leaves no half-finished rename behind, and two people creating the same new tag at once no longer get a server error.
- Bulk export includes your favorites in the image library.
- The pyramid example slide has a proper Dutch title in the Dutch interface.
Export
- The PPTX export option says what it does: every slide except video becomes one image, not an editable PowerPoint slide.
No database migrations between 1.38.1 and 1.38.2. The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.38.2 release on GitHub.
-
v1.38.1
The presenter view speaks the language of the session and the Q&A promote buttons follow the server, library copies keep their translations and tags, and saves that fail halfway leave nothing half-changed.
A patch with fixes only. One of them changes who sees what: an editor on a deck who is not a workspace admin now sees the Q&A promote buttons. Nothing to do when you update.
Presenting
- The presenter view speaks the language of the session. The open or closed status of a poll or Likert question on stage was always in Dutch, and four field labels in the English interface were half Dutch.
- The Q&A buttons in the notes view follow the server’s rule. Add next slide and Add to end now appear for anyone who can edit the deck, the owner and editor collaborators; until this version only workspace admins saw them, although the server allowed more. Remove stays with workspace admins. See Q&A.
Libraries
- A copied library item carries the whole item, including its translations and tags, which a copy used to lose.
- The library shows the right dates and formats for its items.
- Replacing an item’s tags or a collection’s contents happens in one step. If it fails halfway, nothing is left half-changed.
- A tag name that is too long gets a clear error instead of a server error.
Dutch copy
- One spelling per word in the Dutch interface. Six compound words in the slide-type copy were written as two words, and the star in the image library had two names.
No database migrations between 1.38.0 and 1.38.1. The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.38.1 release on GitHub.
-
v1.38.0
The Share dialog gets tabs named after the audience, with the embed code beside the public link; the Node install path states and enforces its PostgreSQL requirement; and a round of fixes for AI, export, the editor and the API.
Sharing a deck now starts from who you are sharing it with, and installing Deckyard without Docker tells you up front what it needs instead of failing later. The rest is fixes. Update if you install on Node without Docker, or turn AI off with
AI_ENABLED=false.Share by audience
- The Share dialog has three tabs: Team, Guests and Public. Each one is the place for that audience, so you no longer have to know which modal holds which link.
- The embed code sits beside the public link. The Public tab shows a published deck’s page link and its embed player as two equal rows, with the iframe and SDK snippets underneath and one language choice for all of them. It used to be tucked away behind the public link.
Installing on Node: PostgreSQL is stated and enforced
- Deckyard refuses to start without a reachable, migrated PostgreSQL, and says which connection it tried and what to do about it. Until this version a missing database ended in an unreadable error, and a database without the schema was worse: the server started, opened the browser and answered every request with an error.
npm run db:migrateis part of installing. The one-line installer runs it before it starts anything, and names the PostgreSQL requirement before it spends minutes onnpm install. The Docker path brings its own database and is unchanged. See the quickstart.
Fixes
AI_ENABLED=falsenow removes every AI entry point, not most of them.- Cancelling an AI analysis stops it on the server. Until this version only the browser stopped waiting; the analysis ran on and could still add suggestions as comments.
- The video placeholder in a PPTX export speaks the deck’s language. It was always Dutch, also in an English deck.
- The editor’s top bar fits at every width. It overflowed below roughly 870 pixels and just above each breakpoint; it now holds down to 341 pixels.
- A malformed id in the API or over MCP is a 404, not a 500. A presentation, collaborator, version or comment id that cannot exist now comes back as “not found”.
- Sandbox instances: a guest is a real user and owns the decks it creates, the first-visitor cleanup no longer leaks guest sessions, and the example decks were rewritten. Only relevant if you run a sandbox.
No database migrations between 1.37.0 and 1.38.0. On the Node path, keep running
npm run db:migrateafter each update, as before: the server now refuses to start if you forget. The built-in slide-type count is unchanged at 35.For the complete, commit-level list, see the 1.38.0 release on GitHub.
-
v1.37.0
Edit a slide in the slide library with the full slide form instead of only its name; favorites in the library are remembered, per user; and a security fix for library tags.
Editing a slide in the slide library used to mean changing its name and description, or placing it in a throwaway deck to change anything else. Now Edit opens the full slide form. This release also fixes library favorites and closes a hole in how library tags are checked. Update if more than one person signs in to your install.
Edit a library slide with the full slide form
- Edit opens the same form you use in a deck, for that slide’s type: nested cards and members, images through the usual picker, background, the Layout chip and the accessibility fields, with Undo and Redo. Saving writes back to the same library item; you no longer need a temporary deck.
- If you may not change a shared team item, because you did not create it and are not an admin, Edit stays greyed out with the reason, and Duplicate to my library makes a personal copy and opens the editor on it.
- Two people saving the same item at once get a conflict beside Save instead of overwriting each other, and closing the editor with unsaved changes asks first.
Favorites are remembered
- The star in the slide library now sticks. Until this version it looked set and was gone after a reload. Favorites are per user: yours are yours, and a colleague sees their own.
Security: library tags follow the item’s permissions
- Until this version, a signed-in user could change the tags of a library item that was not theirs. That is closed. Tags now follow the same rule as the rest of the item: only someone who may change the item can change its tags, and another person’s personal item is not reachable through the API at all.
- Tags are read-only in the library preview when you may not edit the item, like the description.
No database migrations between 1.36.1 and 1.37.0; a normal upgrade is enough. The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.37.0 release on GitHub.
-
v1.36.1
A security fix for the slide library: a personal item can only be changed or deleted by its owner, and a shared team item by its creator or an admin.
A patch with one change, and it is a security fix. Update if more than one person signs in to your install. If you are still on 1.35.0 or earlier, read the 1.36.0 notes first: the trash cleanup it introduces runs on the first start.
Slide library permissions are enforced on the server
- Until this version, a signed-in user could change or delete a colleague’s personal library item through the API. That is closed. Every change to a library item now goes through one server-side check: you can change or delete only your own personal items, and a shared team item only if you created it or are an admin.
- Two edits at the same time no longer overwrite each other silently. The second save gets a conflict instead of replacing the first.
- Editing an item’s content keeps its other language versions in step, instead of leaving them on the old content.
- Saving a custom-HTML slide to the library follows the editor’s rule: only people allowed to author custom HTML can create or change one there.
For integrations
- Changing a library item’s name, description or content through the API needs its revision. Every item now carries a
revision; send it inIf-Match. Without it the server answers 428, with an outdated one 409. An update that carries a field the library does not know is refused with 400. The app itself already does this.
The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.36.1 release on GitHub.
-
v1.36.0
The trash now deletes what it says it deletes, after a retention window you can set; an image picked from ImageKit becomes your own media before it reaches a slide; and image blocks, text blocks and the editor lay out the way you read them.
The trash page has always said that trashed presentations are deleted after 30 days, and until this release nothing deleted them. Now something does. That is the change to read before you update; the rest is an ImageKit picker that stops tying decks to an outside CDN, and a round of layout and editor fixes.
Check your trash before you update. From this version on, a daily cleanup permanently deletes every presentation that has been in the trash longer than
TRASH_RETENTION_DAYS(default 30 days), thumbnails included. The cleanup also runs as soon as the server starts, so the first start after the upgrade deletes the whole backlog at once: everything that has been in the trash for longer than the window on your install is gone within minutes of booting. To keep that backlog, restore what you need first, or setTRASH_RETENTION_DAYShigher before you deploy. There is no value that switches the cleanup off;0, a negative number or anything that is not a number falls back to 30.The trash keeps its promise
- Trashed presentations are deleted after the retention window. The cleanup is the same deletion as Delete permanently, one presentation at a time, thumbnails included. It runs once a day, so a presentation can outlive its window by up to a day.
- Restoring stops the clock. A presentation restored before its window ends stays; move it to the trash again and it starts a fresh window.
- The trash page states the configured number, in every language. An install with
TRASH_RETENTION_DAYS=7says 7 days. See Trash & Recovery and the data retention settings. - Permanent deletion only applies to a presentation in the trash. The app refuses to permanently delete a presentation that has not been trashed first, and asking it to restore one that is not in the trash now answers 409
not_trashedinstead of 400.
An ImageKit pick becomes your own media
- Choosing an image from ImageKit copies it into your install’s own media first, and the slide points at that copy. A deck no longer breaks when the file is moved, renamed or deleted in ImageKit. If the copy fails, the picker stays open with the reason next to “Use this image” and the slide is left as it was. Decks that already hold ImageKit URLs are not changed.
- The copy is an upload, so it needs uploads. On an install with
DISABLE_UPLOADS=1, or in demo or sandbox mode, the picker says so instead of placing the image.
Layout and editing
- Image blocks use the space they have. On an image-blocks slide that keeps each picture’s original shape, the images are sized to the room under the heading: four landscape pictures fit on one row again, a second row no longer pushes the content over the heading, and the titles under the pictures line up.
- A collection under a heading centres in the space below it. Text blocks with a few rows no longer leave an empty band at the bottom of the slide, and a table centres under its caption; content that is too long still starts under the heading. The text-blocks heading is centred by default, with left alignment still in the Layout chip.
- Formatting is visible while you type. The formatting toolbar appears at the cursor as soon as you start editing a rich-text field, empty ones included, so you can switch on bold or italic before typing. Links still need selected text.
- Text-blocks fields are in reading order: row heading and colour, the blocks, then the arrow to the next row, with the bottom subheading after all rows. The first row gets its heading chip too.
Smaller changes
- Importing a file that is not a
.deckbundle gives a plain reason (“the file is not a zip archive”) instead of a library error, and a bundle with a brokenmanifest.jsonordeck.jsonnames the entry that is broken. - Library tiles in the slide picker keep their own content when you change the preview mode or background.
No security fixes in this release; 1.36.1, released the same day, is one, so update straight to that. The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.36.0 release on GitHub.
-
v1.35.0
A .deck file carries the whole deck, every language, theme and custom slide types included; the reader export reads as a document with real lists, quotes, asides and figures; and a numbered list typed in the editor no longer shows 1, 1, 1.
One day after 1.34.0, two tracks land together: the second half of the portable deck, and the reader projection that turns a deck into a document. One breaking change, in the shape of the deck export, and one fix that is worth updating for on its own.
Check your integrations before you update. The JSON export and the
.deckbundle now carry every language of a deck, so?lang=on the JSON export no longer selects one;contentis the dominant language./api/v1/schema/deck.jsondescribes that export instead of the stored model:format,versionandtitleare required,id,schemaVersion,created,modified,settingsanddescriptionare gone, and slides carry noid. An import refuses a deck whose language, or one of its translation tags, the instance does not support, or whoselangcontradicts the request, instead of silently repairing it. Two smaller repairs that used to happen silently stopped too: an action button with a bare domain (example.com) or atel:link, and a LinkedIn link withouthttps://, no longer render; the editor shows a hint next to the field so you can fix the value. Deckyard is in beta, so these ship in a minor version.Numbered lists number correctly
- A numbered list typed in the editor is one list. Typing
1. First, Enter,2. Secondused to leave loose paragraphs until you left the field, and a stored list with blank lines between its items rendered as three lists that each said “1.”. Enter at the end of a line that starts with1.or-now turns it into a list item on the spot and opens the next one, blank lines between items no longer split a list, and a list that starts at 3 shows 3. Decks already stored in the split shape render correctly without a change.
A .deck file carries the whole deck
- Export and import a
.deckfrom the app. The export menu has a.deckrow under Data & bundle, and the new-presentation dialog has an Import .deck tab, the default. If something did not come along, the dialog stays open with the reason and an Open presentation button; a broken bundle gives an error at the file input. - Every language, the notes, the timing and the visibility travel. The JSON and
.deckexports carry all language versions plus speaker notes, duration and visibility per slide; an import restores the translations as language versions. - The deck’s theme and its custom slide types travel too. A deck on an organisation theme carries that theme:
theme.json, the logos, and the curated Google fonts as files (Adobe, Monotype and uploaded fonts by name only, with the reason listed). Slide types made in Settings > Slide Types that the deck uses ride along. Whoever may manage themes and slide types sees one checkbox to install them, off by default; an existing theme or type is never overwritten, a taken slug gets-2. Without installing, the deck lands on the default theme and those slides import as placeholders that say the definition is in the bundle. Export, import, export produces the same bundle byte for byte. - A slide on a custom database slide type round-trips through JSON as itself again, and its translations come along in the JSON and
.deckexports.
The reader reads as a document
The reader view (
/p/…/reader) and the reflowable HTML export now follow the structure each slide type declares, instead of guessing from field names.- Lists, quotes, asides and footers are what they say. A numbered list slide is a numbered list with visible numbers; poll and Likert options are lines, not subheadings; a quote is a
<blockquote>with the name and role in a<footer>; notes, tips and warnings are an<aside>that names its kind in the deck’s language; a byline or logo name is a<figcaption>under the picture; the first column of a table is a row header for screen readers. - Copy reads in the deck’s language. A callout without a label is named by its kind (“Key point”, “Let op”), in the table of contents too, and a chart’s data table gets a summary in the deck’s language (“Line chart with 5 points. Min: 25. Max: 85.”), which is also what screen readers get on the slide itself.
- Values read as values. A KPI reads “98%”, a countdown duration is a
<time>(1:30), a Likert slider names its scale (1 “Useless” to 10 “Essential”), an action or map link jumps to its slide (“Slide 3”), a social link is a real link with its label as text, and the end slide’s e-mail address is amailto:. - A custom-HTML slide reads as its content, not as a code block showing its HTML and CSS. The first heading in the markup names it in the table of contents; inline styles and the CSS field stay on the canvas and drop out of the reader.
- Pictures read true or nothing. A picture gets only a real alt text, explicit or the name of a logo or portrait, never a caption or file name; an image set or gallery is one figure group with one caption; a decorative set hides all its pictures.
- Publishing refuses a meaningful picture without alt text, inline next to Publish, naming the slide and the field (API v1: 422
missing_altwith details). It used to be a “publish anyway” confirm for two slide types.
Print and handouts
- The printed handout is the reader’s projection: the same content and structure, figures, asides and lists included, in the deck’s language. The print wrapper is now
reader-slide; the oldprint-slideand per-type print classes are gone, so custom print CSS keyed on those needs updating.
Smaller changes
- Choosing an image on an instance with more than one source (library, gradients, ImageKit) shows a card per source with a description; ImageKit is the primary choice, and the background field says what the picture is for.
- Every HTML output (presenter, export, embed, print, reader) marks the slide wrapper with
data-slide-type, the reader marks each block withdata-field, and the variant of a callout, list or comparison and the tone of a matrix cell are attributes, for anyone styling or reading the HTML. deckyard-mcp --helplists every tool and prompt the server registers, fork tools included, so the list never lags the code.
For forks and slide-type authors
- New optional declarations:
markup: trueon a code field,nameKeyon an image field,unitKey,hrefKey,headingKey,duration: { secondsKey },scale, theurlandemailfield types,orderedWhen,rowHeader: 'first',role: 'aside'withkindKey,role: 'attribution',termWhen,defaultFromOptionwithcopyKey, adatasetSummaryfunction, andsemantic: trueon an enum. Seedocs/developer/slide-types.mdanddocs/reference/reflowable-html-export.mdin core. - The
.deckbundle isbundleVersion3 (docs/reference/deck-bundle.md); a bundle with an unknown version is refused rather than half-read. Import responses saybundledThemeandbundledSlideTypeswhen something was carried but not installed.
No security fixes in this release. The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.35.0 release on GitHub.
- A numbered list typed in the editor is one list. Typing
-
v1.34.0
Custom slide types now show on shared links, in follow-along and in slide-type previews, exports lay out image rows and emoji as the editor does, and the reader export gives every slide a real heading.
A release with one new behaviour for the reader export and a set of fixes for slide types that your own instance adds. Nothing to do beyond updating.
Custom slide types show up everywhere
A slide type that your instance adds itself, or one built in the slide-type builder, is drawn by the server. A few places never asked for that drawing and showed an empty placeholder instead.
- Shared links, follow-along and the speaker-notes companion now show those slides to viewers who are not logged in. On a password link, the slides appear once the password has been entered.
- The slide-type settings and the Insert slide picker show a thumbnail for them, where they used to show an empty tile.
Exports match the editor
- Image blocks with images at their original aspect ratio stay on one row in PNG, PPTX, PDF and print exports. Before, four landscape images could wrap to two rows and cover the title.
- Emoji render in colour in exports from the Docker image, instead of as empty boxes.
Every slide in the reader export has a heading
- The visible heading of a section is the slide’s title field, declared per slide type. A quote slide no longer turns its quote into the heading, and an image slide uses its title, not its caption.
- A slide without a title still gets a heading for screen readers, named after the slide, so the outline of the document stays complete. Only visible headings are numbered.
- Custom slide types get a visible heading once they declare which field is the heading. Without that, they get the hidden one.
The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.34.0 release on GitHub.
-
v1.33.5
Saved slides in your slide library show their full content again, including slides saved before the slide format changed.
A fix for the slide library. Nothing to do beyond updating.
Older saved slides show their content again
- A slide saved to the library before a slide type changed its shape now shows everything it holds. Before, such an item could show only its title and subtitle, for example a team slide without its people, both in the library preview and when you inserted it into a deck.
- Inserting such an item gives you the current form of the slide, so it edits like any other slide of that type.
- Decks that already received one of those slides get the content back when they are opened, as long as they have not been saved since.
The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.33.5 release on GitHub.
-
v1.33.4
The "+ Subtitle" buttons on a slide work again on your own slide types: a click adds the field and puts the cursor in it.
A fix for installations with their own slide types. Nothing to do beyond updating.
The ”+” buttons on your own slide types add the field again
- On a custom slide type, a click on ”+ Subtitle”, ”+ Meta” and the like now adds that field and puts the cursor in it. Before, the click did nothing on any slide type your installation defines itself, including your own version of a built-in type, because the slide was still loading from the server when the editor looked for the new field.
- Text and rich-text fields both work, and typed text stays after saving.
- If you click elsewhere while the slide is still loading, nothing is left behind on the slide you started on.
For installations that extend Deckyard in code: the
slide-server-renderedDOM event is gone. To wait for a server-rendered slide, useslideRendered(el)fromclient/lib/slide-runtime/slide-render.js, which resolvestrueonce the markup is in andfalseif it never arrives.The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.33.4 release on GitHub.
-
v1.33.3
Opening a shared deck no longer blocks your colleagues: a slide is only locked once you actually change it, and the lock lets go when you stop.
A fix for teams editing the same deck, with collaborative live edits off (the default). Nothing to do beyond updating.
A slide is locked when you edit it, not when you look at it
- Opening a deck or clicking through its slides no longer locks anything. Before, whoever opened a shared deck first held slide 1 for as long as their tab stayed open, so everyone after them saw “This slide is being edited by someone else” and read it as “you may not edit this”.
- The first change you make to a slide takes the lock. Changing the deck title or its settings does not.
- A lock lets go on its own: two minutes after your last change, as soon as you switch to another tab or window, and when you move to another slide. Pending changes are saved first. Start typing again and the lock comes back without you noticing.
- If someone else is editing the slide, your change is not kept: you see who holds it, and the slide shows their version again, presenter notes included.
- A lock left behind by a closed laptop or a crashed tab no longer blocks the slide until you reload. Once it has expired on the server, the slide becomes editable within half a minute.
The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.33.3 release on GitHub.
-
v1.33.2
Copy beside an image sits on the middle of its column, so a short title and paragraph balance against the picture instead of hanging from the top.
A single layout fix, the third release today. Nothing to do beyond updating; existing slides pick it up on their own.
Text beside an image is centred vertically
- On an image-text slide, the copy now sits in the vertical middle of its column: equal space above the title and below the last line, the aside included. This holds for every image width and with the image on either side. Short copy next to an illustration no longer leaves the bottom half of the column empty.
- The image-set slide does the same in its beside layout, where the images stand in a stack next to the text.
- Copy that is too long still starts at the top, so the title never slides off the slide.
- The corner layout and the image-set layouts with a row of images keep the text at the top. There the text belongs to the image above or beside it, and the open space underneath is part of the design.
- This is the default, not a new setting. If your fork sets its own vertical alignment on the copy column in its CSS, that value still wins.
The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.33.2 release on GitHub.
-
v1.33.1
Fullscreen on the published page and in the presenter: no scrollbar, and both bars return as overlays when you move the mouse.
A single fix, released the same day as 1.33.0. Nothing else changed, and there is nothing to do beyond updating. The reason to be on 1.33.x at all is in 1.33.0.
Fullscreen fills the screen
- On the published page, fullscreen hides both bars. Pressing F used to leave the control row hanging below the slide with a scrollbar to reach it. Now the title bar and the control row are hidden, the slide fills the screen, and both bars slide in as overlays when you move the mouse, without shifting the slide. They fade out again after a few seconds.
- A window that fills the screen counts as fullscreen too. Safari’s green button and F11 do not use the browser’s fullscreen mode, so the bars used to stay in place. The page now treats a screen-filling window the same way.
- The arrow keys, space and a clicker do not bring the bars back, so they no longer flash on every slide. Tabbing into a bar does, and a bar stays up while the mouse rests on it.
- The presenter follows the same rules. In fullscreen its top bar now returns along with the progress bar, and the slide uses the full height instead of leaving room for a bar that is hidden.
?ui=minis unchanged: it has no bars, and F still works there.
The built-in slide-type count is unchanged at 35.
For the complete, commit-level list, see the 1.33.1 release on GitHub.
-
v1.33.0
A new slide type for two or three images, speaker notes and a theme template in the PowerPoint export, countdowns that start when you reach them and run on the published page, and one breaking change to chart data.
Ten days of work, most of it on two fronts. The PowerPoint export starts carrying more of the deck: speaker notes, and a template built from your theme. And the slides you present with got a round of repairs from live use, from countdown timers to a vote tally that a latecomer could not see.
Check your charts before you update. The first row of a chart’s data is now always the header and is never plotted. A stored chart whose first row was data will read that row as column names and lose it as a point. Run
node scripts/scan-chart-headers.js(it reads Postgres via.env, or deck JSON with--dir <path>) and look at each hit: a year header that used to be misread needs nothing, genuinely headerless data needs one header row added in the grid editor. Deckyard is in beta, so this breaking change ships in a minor version.A new slide type for several images
- Images + text (
image-set-slide) puts two or three images beside, above or below the text. The layout tiles “Row above”, “Row below” and “Beside the text” now switch to this type. image-text-slideis one image. Decks that stored several images on it are upgraded when they are read, with no migration to run. An API client that wroteimages[],imageFitortextColumnsto that type should write animage-set-slideinstead.
PowerPoint export
- Speaker notes travel with the PPTX, as PowerPoint notes on each slide. They used to arrive only as a separate
notes.mdornotes.docxin the handoff zip. The document is also authored under your app name now. - A new export row, “PPTX template”, downloads an empty
.pptxwith layouts from the deck’s theme: background, fonts, text colours and logo, set per slide. It is not a master that restyles an existing presentation.
Charts
- Bars land on the gridline that names them. A bar chart uses one axis per plot, so a 7 no longer sits on the 8 line; line-chart gridlines stay inside the plot, and a negative bar shows up where it used to disappear.
- Pasting into the top-left cell of the grid reads the first row as column names, and the AI writes plain years as series names again.
- Live-data bindings and quote attribution write to a field that is actually rendered.
Countdowns and live sessions
- An auto-start countdown starts when you reach its slide, not when the presentation opens. Before, every timer in the deck ran from the moment you started presenting. Leaving the slide does not pause it.
- Countdowns run on the published page and in the embed, with Start, Pause and Reset. They used to show their start time and never move.
- The digits sit in the middle of the slide, with or without a title, in the theme’s heading font and weight. The editor preview no longer flickers between two sizes.
- A screen or phone that joins in the middle of a vote sees the current tally instead of “Total: 0” until the next vote comes in.
Smaller repairs
- The ImageKit library shows the newest files first, tag chips included. A large library used to show only its oldest sixty.
- A backup export is complete: it includes the author photos on quote slides, no longer downloads call-to-action links as images, and includes uploads when
UPLOADS_DIRis set. - A custom slide type made in Settings gets the same background, accessibility and logo controls as a built-in one, and can now be created through the public API and the MCP tools instead of failing with
Unknown slide type. - The share dialog holds one height while it loads. The chart, KPI and Likert tiles in the slide picker show a real example, and an English countdown says “Time!”.
- The reader view names a KPI item by its label and shows a video slide as “Video” with a link instead of a video ID.
For forks and integrators
create_presentation_from_slidesrefuses a key the type does not declare (unknown_field) instead of dropping it, and accepts everyexamplefromget_slide_types.update_slidewith atyperefuses a type change it cannot map.- Every slide is created by one factory, so a deck from an import, the AI or an agent gets the same defaults as an editor insert, per language; an agent-made poll slide now gets a
pollId. - A slide type declares how faithfully it exports (
fidelity: { pptx }); a file-based type without it logs a warning at start-up. Seedocs/reference/slide-type-fidelity.mdin core. - New optional field declarations:
itemLabelFieldon an items field,mediaRefon a string field,foldUnofferedToon an enum. A theme can declare the background its slides start on (defaultBackground).
No security fixes in this release. The built-in slide-type count goes from 34 to 35.
For the complete, commit-level list, see the 1.33.0 release on GitHub.
- Images + text (
-
v1.32.0
A fork that adds its own MCP tools is handed the storage scope it needs, instead of copying four lines of core logic and watching them drift.
A one-item release, and the item is for people running a fork. Nothing changes in the editor, in a presentation or in an export; if you run Deckyard as it comes, this update is optional.
For forks and integrators
- The custom-tools seam hands over
storageScopeOf(context). Every facade underserver/storage/has taken a storage scope since 1.4.0 and refuses a barerepoRootstring, but the only builder for that scope lived inside core and never crossed theregisterCustomseam. A fork tool that wanted to reach storage could therefore passctx.repoRootand fail for every keyed caller, or copy core’s logic into the fork and re-copy it on the next merge.storageScopeOfnow sits on the seam’sctx, next torepoRootrather than in its place:repoRootis still the disk path for themes and uploads, it is just not a storage argument. Core’sdocs/reference/mcp-server.mdshows the call.
No breaking changes and no security fixes in this release. The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.32.0 release on GitHub.
- The custom-tools seam hands over
-
v1.31.0
A theme can carry a light and a dark logo and Deckyard picks the one that is visible on each slide; and a translation that has been showing the wrong slide since May is repaired by a migration.
Two days after 1.30.0, a release about things a deck knows about itself. A theme can now hand over more than one logo and let Deckyard decide which one is legible on the slide at hand. The image library opens on the upload, not on the grid. And a translation that quietly fell behind its source in May comes back into line.
Run
npm run migrateafter updating. Migration 081 takes the schema from 12 to 13 and repairs language versions that a May migration skipped: a translation showing an archived slide where the main language shows a timeline becomes a timeline again. Decks are also healed on read, so nothing stays broken while you wait, but the migration is what makes it permanent.A theme can carry two logos
logoOnDarkandlogoOnLightnext toassets.logo, plustitleLogoOn*for the title slide. Deckyard reads the slide’s own surface - background colour, or a background photo - and places the mark that is visible there. A theme with one mark behaves exactly as before, so this costs you nothing until you want it.
The image library and the slides
- The picker leads with uploading. The dropzone and the URL field sit on one row at the top, with the grid underneath. A URL you paste no longer slides away under your fingers.
- A card over a photo gets a real scrim. Text on a card stack over a background image was sitting on a glassy tint; it now has a scrim that makes it readable.
Translations catch up
- Every language version migrates, not only the dominant one. The May migration touched the source and left the translations behind, which is why one language could be showing an archived slide where the others show a timeline. Migration 081 fixes the stored deck, and a read repairs it in the meantime.
- An old slide-type name is rewritten on every read. A deck still carrying a retired type name loads without anyone running a script, on every backend and in every language version.
- The translation prompt treats an undeclared text key as prose. Such a field was being skipped; it is now translated into every language.
Smaller repairs
- The email default locale in settings rolls back to what the server holds when saving fails, instead of showing a value that was never stored.
- OpenAI-compatible providers get the caller’s token budget, so a long generation is no longer cut off at a fixed default.
For forks and integrators
err.detailshas a fixed shape per error code. An API consumer can switch oncodeand know which fieldsdetailscarries.- In MCP, a tool’s gate belongs to its name. A fork registrar that only wraps a core tool keeps that tool’s permission and
readOnlyflag instead of dropping them. An explicitpermission: nullon a gated tool now throws at start-up rather than silently opening it. This only matters with acustom/mcp-tools.jsof your own. - Nothing breaks: no breaking change and no security fix in this release.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.31.0 release on GitHub.
-
v1.30.0
Every refusal in Deckyard now appears where it was caused - at the field, next to the button, in the place the rows would have been - and alt text in the image library follows the deck's own languages.
A small release, three days of work on one idea: a message about something you still have to fix should not be a toast that disappears while you read it. 1.29.0 started this at the save button; 1.30.0 finishes it across the editor, the settings screens and the sign-in pages. The image library also stops assuming your deck is Dutch and English.
There is no reason to hurry with this one. Nothing is broken in 1.29.0 that this repairs, and nothing changes shape for a fork.
A refusal stays where you caused it
- A form that says no says it at the field. An empty name, an invalid colour, the wrong file type: the message sits at the control, with focus moved there, instead of in a corner of the screen.
- An API that refuses a save says so next to the Save button. Settings tabs, email templates, the font and theme editors and the library save all keep the refusal visible until your next attempt, so you can read it while you fix the thing it names.
- Signing in, resetting a password, unlocking a shared deck and joining as a guest do the same. These were the last screens still using a toast for something the reader has to act on.
- A list that will not load says so in its own place. The share modal, the comments, the trash and the slide library show the failure where the rows would have been, and they do not steal your focus to do it.
Alt text follows the deck’s languages
- One alt-text input per language the deck has, in the image detail and at upload, instead of a fixed Dutch-and-English pair. The AI generator fills exactly those languages, and picking an image seeds every language buffer rather than one.
For forks and integrators
- Nothing breaks.
createInlineError()and the doctrine indocs/reference/feedback-surfaces.mdare now the one way to show a refusal, and a guard test rejects a hand-rolled*-errorclass. This only matters if your fork ships editor panels of its own.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.30.0 release on GitHub.
-
v1.29.0
A deck no longer has two languages but a list of them, each one a version you can edit, present and share; and every embed since 1.0.0 has been ignoring its own configuration, which this release fixes.
This is the release where a deck stops being “the Dutch one or the English one” and becomes a deck with a list of language versions. One menu in the editor topbar shows which version you are editing, what other versions exist, and how far each one has been translated. It is also the release that repairs the embed: the configuration you pass to an embedded deck has not reached it since 1.0.0.
Take this update if you have an embed running anywhere. The boot payload the embed page hands to the player went through HTML escaping into a raw-text script block, so it never parsed and every embed fell back to defaults. Slide count, language switch,
start,loop,uiand a configuredallowedOriginslist now do what you set them to. An empty origin list still allows all origins, as documented.Take it too if you have the MCP server enabled. A call over SSE is now scoped to the organization of the API key it authenticated with, and tool calls are gated on that key’s permissions and quota.
A deck’s languages are a list
- One language menu, one version per language. The editor topbar’s fixed
NL|ENpair is gone. In its place a single button names the version you are editing, in its own language (“Nederlands”), and opens onto every version the deck has plus the languages you can add. A deck with one language shows one entry; a deck with five shows five. - The source version stays put while you edit a translation. Switching to a translation no longer moves the deck’s source out from under you, and you can promote another version to source from the same menu when the translation has become the one you maintain.
- The translate modals name their source and target, so you can see which direction you are about to run before you run it, and per language you get a translation-progress figure derived from the deck itself rather than a guess.
- Nested item texts are translated too. Text blocks that carry rows of blocks (
rows[].blocks[]) were skipped by the translator and came back in the source language.
The viewer and the follow codes follow the versions
/p/and the embed show one language button per version that exists, instead of a fixed pair that could offer a language the deck does not have.- A session mints one follow code per version, so an audience member joining in their language lands in that language.
- The presenter console renders the next-slide thumbnail in the deck’s language, which it previously drew in the interface language.
New on a slide
callout-slide, the admonition family. Note, tip, warning and the rest, as a slide of their own.- Comparison slides have sub-variants: versus, before-after, pros-cons and tradeoff, so the layout follows what you are actually comparing.
- Aside insets put a contrast block inside a slide, on the text, list and image-and-text types, without needing a slide of its own.
- The title slide has its own typographic step (a size above the rest of the scale), and the cover steps down a size by itself when the text will not fit rather than overflowing.
Repairs you will notice
- Deck thumbnails load again, and they are content-addressed, so a deck you just changed shows a current thumbnail instead of a cached old one.
- A refused save says which field refused it, next to the button that refused, with focus moved to the field. Local form refusals now sit inline at the field rather than in a toast that disappears while you read it.
- An error announces as an error. A failure message is announced as such to a screen reader, Escape closes a message, and a message carrying a button stays up until you have used the button.
For forks and integrators
- ⚠ Two CSS class renames.
.slide-title-universalbecomes.slide-titleand.slide-lijstjebecomes.slide-liston the title and list types, and.sfi-card-kickerbecomes.sfi-card-titleon the feedback and follow-invite types. Deckyard’s own stylesheets and the core themes move with them; only a fork with its own rules on the old names has to rename. Thetsu-*and.lijst*families are unchanged. Deckyard stays on1.xwhile the beta badge is up, so a minor release carrying breaking changes is the intended shape rather than a mistake. - Custom slide-type definitions are validated, and
scaffoldonly writes definitions that pass. A slide type stored in the database gets its CSS scoped to its own root, and there is a stated scoping convention for file-based JS types plus a shared partials library for eyebrows, badges and highlights. - An authenticated but unauthorized API call gets 403, not 404. A storage refusal now carries
detailswithfield,index,itemIndexandreason, so a client can point at what was wrong.
The built-in slide-type count goes from 33 to 34 with
callout-slide.For the complete, commit-level list, see the 1.29.0 release on GitHub.
- One language menu, one version per language. The editor topbar’s fixed
-
v1.28.0
One command renders your theme across every slide type on a single sheet, Companion is the same product name in all twelve languages, and the background migration script finally works on Postgres.
The third release of the day, and the one with something new in it: a contact sheet for themes. If you maintain a theme,
npm run theme:preview <theme-id>now renders every slide type against every background that theme offers, in one HTML page you can scroll, so you find out where a theme breaks before an audience does.See a whole theme at once
npm run theme:preview <theme-id>writes a contact sheet. One tile per slide type and background combination, grouped by type, plus a contrast table for the theme’s flat background variants. Until now the only preview was the theme editor’s live one, which renders a database draft across a handful of types; a fork that versions its house style as a file theme incustom/themes/had no preview at all, and nobody saw what a theme did to the thirty-odd types the panel leaves out.- A tile is what an export produces. Every tile goes through the same render path a PDF or PNG export runs, rather than a lookalike that would keep passing the regressions the sheet exists to catch. It needs no running server and no database.
- The sheet walks the matrix the editor offers, including the backgrounds a theme adds on top of a slide type’s own, and it skips a theme-scoped type under a theme that cannot use it. The contrast table scores what is honestly measurable: a variant on a solid or gradient ground gets a ratio, artwork grounds are reported as not measured rather than scored as a failure they do not have.
Translations settle on one word
- “Companion” is one product name in all twelve languages. Nine locales had translated it - “Följevy”, “Begleitansicht”, “Acompañante”, “Compagnon”, “Asystent” and the rest - which made the same view look like a different feature depending on your language. The head noun is now the loanword everywhere, while the modifier stays translated, so German reads “Publikums-Companion” and French “Companion du public”.
- Italian addresses you informally throughout. The locale was already roughly two to one in the tu form, but an earlier sweep had pushed the click verbs to the formal Lei (“Faccia clic”); around 260 strings now read as one register, courtesy possessives and clitics included. A few Italian terms were settled at the same time, including the follow-along invitation, which uses the loanword the way English and Dutch do.
- Polish and Finnish get a handful of rewrites where the earlier pass had left something readable but not idiomatic. Key sets, key order and placeholders are untouched in all 27 files; only the words changed.
For self-hosters
- The legacy background-image migration works on Postgres and covers language versions. The script walked one directory of deck JSON, so a Postgres install migrated nothing and reported a clean zero, and a deck’s translated versions were skipped in both stores - in one production dataset that surface held more legacy slides than the decks themselves. It now takes
--backend auto|file|postgres|bothand walks decks, language versions and library items by construction. If you ran the migration earlier on a file store you do not need to do anything; if you skipped it, it now works in either setup. Version snapshots are left alone unless you pass--include-versions, because that fold is lossy and legacy content renders correctly until the first edit after a restore anyway.
The built-in slide-type count is unchanged at 33.
For the complete, commit-level list, see the 1.28.0 release on GitHub.
-
v1.27.2
A theme you switched off is now really off in every picker, the images field stops jumping under your cursor, and a handful of English labels settle on one spelling.
Three repairs, cut the same day as 1.27.1. The one worth reading is the theme allowlist: it was documented as the visible subset of themes but behaved as a label, so a theme you had withdrawn was still reachable from two of the three pickers that offer one.
A withdrawn theme is actually withdrawn
- The
enabledThemesallowlist is enforced on the server, in every picker.GET /api/themesused to return every theme and tag the ones outside the allowlist as disabled; only the creation grid read that tag, and a “Show all themes” toggle undid it in one click. The editor’s deck-settings select and the “start from a theme” row on Home ignored it entirely. The endpoint now filters, so no picker can offer what the allowlist leaves out, and the soft toggle is gone. - A deck that predates a withdrawal keeps its own theme. The pickers pass the theme the deck is on, and the resolved default theme is always kept in the list, so an organization cannot allowlist itself out of the theme its own new decks are created with.
- Themes you manage stay visible where you manage them. The Themes settings tab asks for the unfiltered list, since it cannot offer a checkbox for a theme it is not allowed to see, and its checkboxes now read the allowlist that is really in force.
- New
ENABLED_THEMESenvironment variable, besideDEFAULT_THEMEand with the same precedence: the app setting wins, then the comma-separated variable, then no allowlist at all.
Fixed
- The “Selected images” list refreshes where it stands. Ticking a preset checkbox in an images field wrote the value but left the list showing the previous one, and the two paths that did redraw rebuilt the entire form, taking focus off the checkbox you had just used. The list now updates in place, and a selection that runs past the field’s maximum unticks the box whose image did not make the cut instead of leaving it looking selected.
- One English per label. Four keys carried a different English in the slide-type registry than in the interface: “Centre label” against “Center label”, a “Title (optional)” for a field that is not marked optional anyway, a Dutch example in an English help text, and a pair of aria-labels that repeated their own label. They agree now, which means a few English labels in the inspector read slightly differently than they did in 1.27.1; the Dutch and the other ten languages are unchanged.
The built-in slide-type count is unchanged at 33.
For the complete, commit-level list, see the 1.27.2 release on GitHub.
- The
-
v1.27.1
A theme can no longer send the PDF exporter out to the network, a bilingual deck keeps both languages through a collaborative edit, a theme variant's photo background reaches the PDF, and the editor shows one background control per slide.
A patch release of repairs, and one of them is about not losing work. A bilingual deck holding a text-blocks slide could silently lose its second language the moment someone opened it in a collaborative session; that is fixed, along with a PDF exporter that dropped theme artwork and an inspector that showed the same background picker twice.
Take this update if anyone but you can edit a theme. A remote URL written into a theme variable reached headless Chrome during a PDF export, which made free-form CSS in the theme editor a server-side request forgery route into whatever your export host can reach. The exporter now strips remote URLs from theme values before the document is rendered.
And take it before the next collab edit if you keep bilingual decks. The dominant language was written into both versions on round-trip, and nothing warned you.
Bilingual decks survive collaboration
- Hidden prose is still prose. The collab codec treated every hidden field as a single machine value shared across languages, but the text-blocks slide keeps 38 hidden mirrors of human-written text, so a round-trip collapsed them to one language. Eight modules held eleven slightly different opinions on what counts as translatable text; they now share one definition - a field is text when its type says so, at every nesting level - and the mirrors translate like the prose they are.
Exports render the theme’s artwork, and only the theme’s artwork
- A theme variant’s image background arrives in the PDF. Two independent bugs blanked it: a local image URL in a theme variable never went through the embed pass that every other image takes, and the gradient rasterizer flattened a photo-with-scrim down to its fallback colour. Right in the editor, empty in the export - now the same in both.
- Theme values cannot send the exporter to the network. The same pass now strips any remote URL from theme variables before the document reaches headless Chrome, closing a server-side request forgery route through free-form CSS in the theme editor.
- Two latent export defects went with it: a class-name substring match that pushed gradients into the PDF as per-pixel shading - seconds per page - and a stylesheet order that let the base theme override a variant’s own text colour in exports only.
A dark background gets readable foregrounds
- A background variant now declares how dark its ground is. Setting a variant’s text colour already says so; the slide now carries the same contrast class the background-image path emits, so per-type treatments key on the luminance instead of recognizing background names. A dark variant no longer produces white cards holding white text.
One background control per slide
- The doubled “Background image” picker is gone. A half-finished unification folded the legacy per-type image pair into the shared control for one slide type only; any type following the developer doc got both. The fold now runs for every type, heals a stored deck on first edit, and clearing a background no longer quietly re-seeds the photo you just removed.
The editor speaks your language throughout
- Dropdown options and shared field labels are translated in all twelve languages. Sixty-six option labels and the eighteen shared field labels - the CTA-button fields, text size, poll and scale close behaviour, the alignment controls - showed raw English in every locale; they now carry the words each language already uses elsewhere in the interface.
The built-in slide-type count is unchanged at 33.
For the complete, commit-level list, see the 1.27.1 release on GitHub.
-
v1.27.0
An instance admin no longer holds silent power in a workspace where they are only a member: the server now scopes the five remaining bypasses to the active organization.
If you run Deckyard with more than one organization, this is a release to take. An instance admin who switched into a workspace where their membership says “member” kept six abilities the interface had already stopped offering them, because the server never followed the client. Those are now closed.
Take this update if you run multi-organization mode. In a single-workspace instance nothing changes: the rule collapses to the check it always made.
Admin power stops at the workspace boundary
- The server scopes the five remaining admin bypasses to the active organization. The previous release hid ten admin affordances that read the instance-wide role instead of the role in the workspace you are actually in, but the requests behind them still succeeded. In that workspace an instance admin could remove an audience question, delete a library image, resolve and edit anyone’s comments, change any deck’s visibility including organization to private, and lock or unlock slides on a deck they did not author. All six checks now ask whether you are an admin of the organization you are in.
- The ten affordances in the client follow the active workspace too, so the button and the request behind it finally answer the same question. A refused permission also renders as a plain refusal rather than a “Something went wrong” heading over a stack trace.
- The e-mail template preview renders in a sandboxed frame. Admin-written markup with substituted fields was being written straight into the settings page, in the same origin as your session. It was never a privilege escalation, since writer and reader hold identical rights, but it was the last place that argument was holding up. As a bonus the preview now shows the actual e-mail: the wrapper carrying the mail’s own font, colour and 600px width used to be discarded on the way in, so you were previewing the settings page’s styling.
Fixed
?limit=abcno longer empties the page. The public v1 API had its own copy of the pagination parser without a guard for a value that is not a number, so a malformed limit produced an empty result and anulllimit in the envelope. Both copies are now one function, which falls back to the default.- All three Q&A views read the same field. The follow page, the presenter’s notes panel and the moderator route each owned a copy of the question list, and they had drifted on which field carries the question’s text. They agree today, so nothing was visibly wrong, but the moderator was one schema change away from deleting a question whose text they had never seen. There is now one accessor, one feed and one set of mutations.
en/is a superset of every locale again. Sixty-two keys existed only in Dutch, and the global slide fields had a separate key per slide type instead of one shared key per field. Both are fixed and translated into all ten fill languages, along with a copy sweep through six of them.
Breaking, for contributors
Deckyard stays on
1.xwhile the beta badge is up, so a minor release carrying breaking changes is the intended shape rather than a mistake. These two touch the translation scripts in the repository, not a running instance:npm run i18n:validateis gone. Every check it made is now a test.- The i18n scripts share one flag vocabulary.
--dry-runand--reportand the bare language argument are replaced by--applyand--json, with reading as the default, so a script only writes when you say so.
The built-in slide-type count is unchanged at 33.
For the complete, commit-level list, see the 1.27.0 release on GitHub.
-
v1.26.0
The interface is now complete in twelve languages, and a poll or scale keeps its answers in one array instead of ten numbered fields.
Deckyard shipped ten translations that were only partly translated: the language was selectable, but somewhere between a third and two thirds of every screen fell back to English. All ten are now finished, so the interface is complete in twelve languages. Alongside that, the last legacy numbered fields in the deck format are gone.
If you write decks through the API or MCP rather than the editor, the poll and likert change below is the one to read; existing decks migrate on their own.
The interface is complete in twelve languages
- German, French, Spanish, Portuguese, Italian, Polish, Finnish, Danish, Swedish and Norwegian are finished. Each went from roughly 1,200 keys, the rest falling back to English, to the full set of about 3,400. English and Dutch were already complete, so twelve languages now render every screen in the language you picked, not just the menus.
Polls and scales keep their answers in one list
- A poll’s answers and a scale’s points are one
optionsarray. They wereoption1throughoption4on a poll andoption1throughoption10on a likert slide, the last numbered field family in the format. Schema step v8 to v9 folds each into a single list of{ text }entries. Existing decks migrate automatically, and votes already cast keep pointing at the answer they were cast on. If you generate decks through the API, MCP or the AI catalogue, writeoptions: [{ text: '...' }]from now on. - Blanking an answer mid-poll no longer re-points the votes. Because the numbered form could have holes, both readers counted votes against the compacted list, so emptying the second answer of a running poll silently moved every vote cast on the third. A real list has no holes, so this stops happening.
- A scale reads as an ordered list, a poll as an unordered one, in the reader and in exports, because the likert array declares that its order carries meaning.
The reader projects structure, not configuration
- Table and dataset slides follow their structure contract. A caption spans the section rather than the table’s width, and a markdown cell renders inline instead of being wrapped in its own paragraph.
- Configuration stopped appearing as prose. An icon name, a media library id, a poll’s target slide and a blob of zoom coordinates are all strings, so the reader printed them as text. A field now declares whether it is readable text. The visible effect: an icon-card grid was headed “rocket” instead of by its title.
- Alt text is no longer said twice. An item’s own image now folds into its figure, the way slide-level images always did, so a team card’s alt text is the picture’s description and not also the card’s heading.
Fixed
- A deck with a custom slide type saves. The Settings builder stores a custom type in the database, but the save path checked slides against the file-based registry, which a database row can never be in. So the editor inserted the slide and every autosave after it answered 400, leaving the deck unsaveable until you removed it. Saves now validate against the organization’s own registry, the same one the viewers and exports already read.
- The
imagesfield is finished as an extension point, so a fork’s own slide types can use the multi-image field; its dead alt path is gone.
The built-in slide-type count is unchanged at 33.
For the complete, commit-level list, see the 1.26.0 release on GitHub.
-
v1.25.0
A share link finally works for the people it exists for: someone without an account gets the deck, its theme, and an answer when verification fails.
A share link’s whole audience is people who are not logged in, and for them the viewer was broken in three ways at once. This release fixes all three, and puts a CSP header on the app shell itself.
If you send share links to people outside your organization, take this update. An anonymous visitor holding a perfectly valid link got “Failed to load presentation”.
Share links work for anonymous visitors
- The deck loads. The viewer fetched it from an id-addressed route behind the login gate, so someone without an account got a 401. It now rides on the one call that proves the link,
POST /api/share/:token/verify, which is also the only place a password can be checked. Slides marked hidden from viewers never leave the server, and ownership stamps, organization and visibility do not either. - The deck’s own theme comes with it. A custom theme is a database row resolved through a route behind the login gate, so every anonymous surface caught a 401 there, swallowed it and rendered the deck on a neutral fallback: silently unbranded. The theme now travels on the payload the capability already authorizes, which covers the share-link viewer, the follow-along audience and the notes companion. Built-in themes are static files the client loads itself.
- A failed verification says something. A guest whose e-mail verification link failed was redirected back and shown nothing at all. There is now a banner between the deck chrome and the slide, because the link is valid and the deck is readable; only the identity step failed. It carries the button to request a new link rather than describing where to find it. One case gets its own answer: a verification token is spent on first use, so clicking the same mail twice while already holding a live session says what happened and offers nothing, rather than telling you to request a link you do not need.
Typography scales with the slide
- The slide type scale is derived from the slide’s own box. It was ten literal pixel values, and the theme multiplier its own comment announced was never built. Every step is now a number of reference pixels turned into a length by the slide’s width, and
--t-slide-text-scaleworks. On the 1600x900 reference canvas, which is every path today, each step resolves to exactly the value it had before; on any other box it is proportional.
Security
- The app shell sends a CSP header, completing the work the previous two releases did for the render paths.
Fixed
- Pasting under a parent lands after its children, not inside them. The nesting rendered correctly, but the deck order interleaved, so the numbering read 1 Parent, 3 Child, 4 Child, 2 Pasted. Duplicating from the form header’s menu had the same bug.
- Guest-join error copy is keyed on the machine code, and the duplicate e-mail guard is dropped.
- The development auth bypass uses a valid address (
dev@local.test).
The built-in slide-type count is unchanged at 33.
For the complete, commit-level list, see the 1.25.0 release on GitHub.
- The deck loads. The viewer fetched it from an id-addressed route behind the login gate, so someone without an account got a 401. It now rides on the one call that proves the link,
-
v1.24.0
Lead capture is removed root and branch, the CSP becomes a real header on published pages and embeds, and 21 API error codes collapse into one.
The lead-capture slide type is gone, along with everything that fed on it. It was a form you could put on a slide but never submit: the marketing consent it gated on was never wired up, so it collected nothing. Deckyard’s beta stance says a half-built feature gets stripped rather than parked, and the plan to collect visitor details properly will replace the code rather than preserve it.
The built-in slide-type count goes from 34 to 33. A deck that stored a lead-capture slide still loads, presents and exports; the slide renders as an archived placeholder that names the type and keeps every field visible. A scan of stored decks on 22 August found none using it.
What went with it
- The
.slide-lead-captureand.lead-capture-*CSS classes are no longer emitted. If you style slide types in a fork, those names are gone. - The
/api/leadsendpoints are removed, along with the lead storage, the GDPR token store and the owner-notification e-mail with its two templates. - The
lead.submittedwebhook is gone, and its admin row with it. Seven configurable events remain. - The
leads.retentionDayssetting, the lead e-mail preference and the admin “Lead capture” card are removed, as are/my-dataand the analytics leads tab. - A migration drops the
lead_submissionsandgdpr_verification_tokenstables. Nothing else read either: share-link guests carry their own verification columns and the analytics erasure path has its own token, both untouched.
This is not a major release:
/api/leadswas never part of/api/v1, and since the type was removed no deck could produce a lead.The CSP becomes a header
- Published pages (
/p/) and embeds (/embed/) send the document CSP as a real HTTP header, not only as a meta tag. Several directives are ignored in meta form, so a policy in a header is the one a browser can act on fully. - hls.js is vendored, closing the last CDN on the render path. Video playback no longer depends on a third-party host being up.
One code for bad input
If your API client branches on an
invalid_<thing>error code, it needs updating. Twenty-one of them collapse intoerror: "invalid"plusdetails.field, which names the input the code used to encode in its suffix. The status is unchanged: 400 throughout. Gone areinvalid_category,invalid_colors,invalid_contact,invalid_email,invalid_fonts,invalid_format,invalid_label,invalid_level,invalid_new_owner,invalid_order,invalid_permission,invalid_permissions,invalid_presentation,invalid_registration_mode,invalid_role,invalid_slide_index,invalid_slug,invalid_source,invalid_style,invalid_usageandinvalid_weight.The four 401 codes stay:
invalid_password,invalid_token,invalid_or_expiredandinvalid_or_revokedsay the credential does not hold, which is a different answer at a different status.Fixed
- Power over a deck reads the owner stamp, not owner-or-creator. Someone who created a deck and then transferred it away, keeping no collaborator row, could still write to it, delete it, reshare it and manage its collaborators. They now get a read-only editor. This is only reachable after an ownership transfer that did not keep the previous owner as a collaborator.
- The never-mounted cookie-consent banner is removed. It was in the code and never appeared.
The built-in slide-type count goes from 34 to 33.
For the complete, commit-level list, see the 1.24.0 release on GitHub.
- The
-
v1.23.0
Every rendered document now carries a Content-Security-Policy, the media provider is called S3 instead of Scaleway, and people are named rather than addressed.
The last two releases took third-party scripts off the render path. This one tells the browser about it: every document Deckyard renders carries a Content-Security-Policy naming the hosts it is allowed to load code from. It is the first of the three statements about third-party origins that the machine running the document can actually refuse.
If you use remote media storage, your environment variables are renamed. The old names keep working until the first release after 2026-11-01, with a boot warning each. Details below.
A policy the browser enforces
- Every render path emits a CSP, assembled in one place and emitted before anything that can load. It is a host allowlist for code and styles rather than an XSS defence, and it says so: the render paths inline their own runtime, so
script-srcandstyle-srcneed'unsafe-inline'. What it refuses is a script from a host nobody decided on, anddefault-src 'none'makes any new kind of fetch fail closed. - The last CDN loads are vendored. pdf.js came from cdnjs into headless Chrome; Prism and KaTeX are served from the vendored copies on every render path, not just in the app.
- Analytics provider ids are validated rather than escaped.
The media provider is S3, not Scaleway
The remote media provider was named after one vendor while being plain S3-compatible underneath.
MEDIA_STORAGE_MODE=scalewaybecomes=s3SCW_ACCESS_KEY/SCW_SECRET_KEY/SCW_BUCKET/SCW_REGION/SCW_ENDPOINT/SCW_CDN_URLbecomeS3_ACCESS_KEY/S3_SECRET_KEY/S3_BUCKET/S3_REGION/S3_ENDPOINT/S3_PUBLIC_URL
The old names are read until the first release after 2026-11-01, only when their
S3_*counterpart is unset, and each one that is read prints a boot warning naming its replacement.S3_ENDPOINTis mandatory under the new names:MEDIA_STORAGE_MODE=s3refuses to boot without it. An untouched legacy install is the one exception, where an unset endpoint is still derived ashttps://s3.<region>.scw.cloud. A partly configured set now warns at boot instead of silently staying on local storage.People have names, not addresses
The identity work that started in 1.16.0 finishes on the surfaces that face other people.
- Comments and slide locks name people with an id and a display name. A comment carries an author pair instead of an address plus a name, and the AI author is flagged by the server rather than by the client comparing addresses.
- Creator and trasher stamps are pairs too, and the e-mail fallback in identity matching is retired.
/api/v1/slide-libraryreturnscreatedByIdinstead ofcreatedBy. The old field was the creator’s e-mail address, disclosed to any API key with library read access. The rest of the public v1 surface already exposed ids only.
Removed
- The presentation-level lock and its access-request queue are gone, along with the
/api/presentations/:id/lock/*routes, thepresentation_locksandlock_requeststables and theUSE_DB_LOCKSvariable. The client had hardcoded slide-level locking; this path was never reached. Slide-level locking is unchanged, and one real bug in it is fixed: releasing a lock read the row without an expiry filter, so an expired lock held by somebody else answered 409 for a lock nobody held.
A transfer of ownership is final
- Someone who transferred a deck away can no longer transfer it back.
POST /api/presentations/:id/transfer-ownershipanswers 401 for an actor holding only the creator stamp; the decision keys on the owner stamp rather than on owner-or-creator. You keep the authorship record, not the power.
For API clients
- Every storage failure answers the status its own register entry states, rather than 400 by default.
parent_not_foundis 404,own_questionis 403, and the internal-failure codes are 5xx. Eight codes are renamed to snake_case tokens. - The reason synonyms collapse to one spelling each.
Fixed
- A download is titled with the deck name. The
<title>of a print, PDF-slides or PNG-export document is now the deck title alone; anything of yours that matched on the old suffix will stop matching. - Escape peels one overlay at a time and hands control back properly, instead of closing more than you asked for.
- Copied slides keep their nesting and get fresh instance ids.
- The more-menu wires each item once, closing before it acts, so a menu item cannot fire twice.
- “Add as second slide” puts the invite second.
- The markdown import buttons stop saying “Import JSON”.
- Code blocks highlight on every render path, and the fork CSS seam serves
custom/styles/and fork fonts.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.23.0 release on GitHub.
- Every render path emits a CSP, assembled in one place and emitted before anything that can load. It is a host allowlist for code and styles rather than an XSS defence, and it says so: the render paths inline their own runtime, so
-
v1.22.0
The built-in theme called "Deckyard" is renamed to "Amethyst", and only one built-in theme still wears the Deckyard mark.
A small release with one rename in it, worth checking if you have decks on the violet built-in theme.
The built-in theme id
deckyardis nowamethyst, labelled “Amethyst”. There is no alias for the old id. A deck stored with"theme": "deckyard"still loads and renders, in the default theme (brand, Forest), but it will fail validation on its next save until you pick a theme again. Custom themes incustom/themes/are unaffected.- Amethyst carries the neutral placeholder logo, like the other archetype themes.
brand(Forest) is now the one built-in theme that wears the Deckyard mark, which is what an archetype meant to be a starting point should look like. - The default theme is decided by the server, not picked client-side. The slide-library compose path used to choose one itself.
Removed
- Two undocumented AI wizard endpoints are gone.
POST /api/ai/wizard-v2and/api/ai/wizard-v2/outlineare removed; the streaming endpoint/api/ai/wizard-v2/streamis the one path and is unchanged. Neither was in the OpenAPI document, so this is only noticeable if you called them directly.
Fixed
- Markdown actually renders in MCP previews and exports. The MCP process never initialized its sanitizer, so anyone previewing or exporting a slide through the MCP server saw lists and formatting as visible
<ul>markup. It does now, and if a sanitizer is missing somewhere there is one warning on stderr instead of silence. - The follow API answers 404 for an unknown presentation and 503 for an unavailable store, where both used to be a 400. If you matched on 400, that needs updating.
- The descriptor-table translation gaps are filled, and the coverage check can now see them, so the next gap fails the build instead of shipping as English in a Dutch interface.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.22.0 release on GitHub.
- Amethyst carries the neutral placeholder logo, like the other archetype themes.
-
v1.21.0
Imported Notion images stop expiring, outgoing webhooks can be signed, and a data request no longer needs an account to complete.
Two things here fix promises that were only half kept: an imported deck whose images died an hour later, and a webhook receiver that had no way to tell a real delivery from a forged one.
Run migrations 075, 076 and 077 with this update. Without 076 the slide library does not start correctly, and without 077 a configured slide-library webhook URL goes missing.
Notion images survive the import
- Images imported from Notion are re-hosted in your own media library. Notion hands out signed URLs that expire in about an hour, and without ImageKit configured Deckyard kept those URLs verbatim, so an imported deck lost its images shortly after import. Each image is now fetched and stored through your configured media provider, and the block points at the durable copy. If an individual image cannot be fetched, the original URL is kept rather than the image being dropped. An instance with ImageKit configured is unchanged.
- Those fetches are SSRF-guarded and enforce a maximum size, because the import is now making outbound requests on a URL from a document.
Webhooks
- Deliveries can be signed. Set
webhooks.signingSecretand every delivery carriesx-sb-signature: sha256=<hmac-of-body>next to the existingx-sb-eventheader. Absent when the secret is unset, so nothing changes until you opt in. There is an admin field for it. actor.idcarries the stable user id, not the address. The address stays available asactor.email, a contact value rather than an identity. If your receiver keyed onactor.idas an address, it needs updating.- The event
slide.added_to_team_libraryis renamedslide.added_to_organization_library, in the payload and in thex-sb-eventheader. There is no alias, so a receiver matching the old string stops recognising it silently. - The user-agent is
Deckyard-Webhook/1, replacingpresentation-system-webhook/1.
Data requests without an account
- The my-data self-service routes are public, with a landing page and a durable token store behind them, so someone can complete an access or erasure request without being able to sign in. In 1.20.0 the verification mail started going out; this is the other half.
leads.retentionDaysis in the admin settings screen, rather than only in a config file.
For API clients
- Public API v1 answers with one error envelope. Every route on the v1 surface now returns the same error shape.
- Publishing runs on one shared core, so the internal publish path and the v1 publish endpoint cannot drift apart.
- The MCP filter argument is
ownership, notscope. It applies tolist_presentationsandlist_recent_comments, in the answer as well; the values are unchanged (owned,shared,all). An old call falls back to the default silently rather than erroring. - The slide library speaks of a shelf. The API takes
shelf: 'personal' | 'organization', and the bulk-export ZIP entry isslide-library/organization.jsonwhere it wasteam.json.
Fixed
- A two-column list caps at six items when a subheading meets three-line titles, instead of overflowing the slide.
- Team-library listings get past a latent 100-row cap, and the authorization guard on them is resolved.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.21.0 release on GitHub.
-
v1.20.0
Feature switches all read the same way round: `AI_ENABLED` instead of `DISABLE_AI`. The old names keep working until November.
Three kill switches were spelled as negatives, so turning something off meant setting a variable to true and reading a configuration file meant working out a double negative. They are now enable-form, defaulting to on.
If you set any of these, plan a rename before 2026-11-01.
DISABLE_AIbecomesAI_ENABLEDDISABLE_UPLOADSbecomesUPLOADS_ENABLEDDISABLE_IMAGE_LIBRARYbecomesIMAGE_LIBRARY_ENABLED
All three default to on. The old spellings are still honoured, inverted, until the first release after 2026-11-01, and each one you have set prints a boot warning naming its replacement and the removal date. When both spellings are set, the new one wins.
docs/reference/feature-flags.mdrecords the rule, so a future flag arrives in the enable form too.GDPR self-service actually delivers
- The verification link for a data request is sent by e-mail. The endpoint minted a token and never delivered it, so it told people “you will receive a verification link” while sending nothing; the self-service access and erasure flow was unreachable in production, which is exactly where it matters legally. An unconfigured mail setup now answers an honest 501 rather than pretending.
Removed
- The on-view refresh mode is gone. It appeared in the data-source settings and was never implemented, so choosing it did nothing.
Fixed
- MCP tool writes go through the maintenance gate. An instance in maintenance mode could still be written to over MCP.
Security
- The unauthorized
datasource:refreshedbroadcast is dropped. A refresh event went out over SSE to listeners who had no claim on the data source. - Analytics report changes are scoped to their own presentation. A report mutation was not checked against the deck it belonged to.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.20.0 release on GitHub.
-
v1.19.0
Every theme lever is a role token now. The per-slide-type tokens are gone, which is worth reading if you maintain a custom theme.
A theme used to be able to reach into individual slide types: there were tokens for KPI tiles, for table variants, for the icon-card grid, for quote text. That let a theme style one slide type and say nothing about the next one, so a new slide type arrived unstyled and every theme had to be updated to catch up. Themes now set roles instead, and each slide type derives its look from those.
If you maintain a custom theme, read the list below. Unknown tokens in an existing theme are harmless (they simply do nothing), so nothing breaks, but a theme that leaned on a removed token will render with the role-derived styling instead of what it asked for.
What changed in the token vocabulary
- KPI tokens are removed.
--t-kpi-tile-{1..4}-*and--t-kpi-delta-*are gone; tiles render the neutral defaults, and a coloured series comes from--t-chart-*. - Table variant tokens are removed.
--t-table-<variant>-*is gone; table planes follow--t-color-accent(plus its contrast) and the mist and raised surfaces. - Icon-card grid tokens are removed.
--t-icon-card-grid-*is gone; the icon plane derives from mist and accent (override with--t-color-accent-soft), card bodies read--t-color-surface-raised, and header text follows the gradient and on-surface roles. - Quote and chapter text tokens are replaced.
--t-quote-text-colorand--t-chapter-text-colorbecome--t-slide-bg-dark-text;--t-quote-author-coloris renamed--t-color-accent-on-dark. --t-list-item-title-letter-spacingis removed.- The legacy aliases are no longer emitted.
--t-primary,--t-accent,--t-bg-darkand--t-brand-1/-2are gone; brand slots--t-color-brand-{1..3}fill frombrandColors.
Fixed
DELETE /api/leads/my-datareaches the erasure handler instead of being routed to the id-addressed row. If you accepted GDPR erasure requests, they were landing in the wrong place.- Fresh invitations no longer show as “expired” in the admin user list.
- A queued export reads its type from the job name, so a PDF job does not come back as the wrong format.
- The remaining error stragglers use the canonical envelope, so an API client sees one error shape everywhere.
- A failing
sendEmailis typed. A misconfiguration answers 501 and an upstream failure stays 502, so “we cannot send mail” and “the mail server rejected it” stop looking the same. - The unused
image-sizetransitive dependency is stubbed out, removing an unpatched package from the tree.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.19.0 release on GitHub.
- KPI tokens are removed.
-
v1.18.0
One word per meaning: a deck has a visibility, the tenant is an organization, and the API says so. Revoking access is now logged and told to the person.
Deckyard used the word “scope” for three different things and “workspace” for two. This release picks one word per meaning and changes the wire, the database values and the documentation to match. If you call the API, one field is renamed; everything else in here is behaviour.
If you use the public API,
scopebecomesvisibility. The values areprivateandorganization;workspaceis gone as a value. The internalPATCH /api/presentations/:id/scopebecomes/:id/visibility, and the webhook eventpresentation.moved_to_workspacebecomespresentation.moved_to_organization, carryingfromVisibilityandtoVisibility. “Workspace” survives as a word people see in the interface; it is no longer a word in the data.- API-key capability lists are
permissions, which is what the settings screen already called them.
Losing access is now visible
- The access log is symmetric. It recorded grants but not revocations, so a collaborator list could change without a trace. Revocations and permission changes are logged, and the person who lost access is told rather than finding out by clicking.
For API clients
- Slide-library tag updates take
{ tags: [...] }only. The other accepted shapes are gone; if your client sent a bare array or a single string, it needs the object. - One JSON entry point for every request body, which also means a non-object body is rejected consistently instead of per route.
Under the hood
- Deck versions, slide locks and the live SSE stream are keyed on the user id, continuing the identity work from 1.16.0 and 1.17.0. Trash and the slide library route their authorization through the same identity match.
- The deck schema moves to v6. Per-field alignment keys that no longer did anything are folded away. Migration runs when a deck is read and is persisted on its next save, so there is nothing to do.
Fixed
- Analytics honours the retention setting, and the dead analytics tables are dropped.
- Device labels work in secretless boot modes, deriving an ephemeral key instead of failing.
- A comparison slide’s subheading sits one step above the column prose, which is what it was always meant to do.
- A list slide re-measures its capacity against the current sizes, so items stop being dropped or crammed after a size change.
- One canonical list of e-mail template types, and a dead export-ready path is gone.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.18.0 release on GitHub.
- API-key capability lists are
-
v1.17.0
Ownership is decided on the user id instead of the e-mail address, anonymous viewers can erase their own analytics data, and the analytics API stops handing out identifiers it should not.
The identity work started in 1.16.0 becomes real here: ownership, workspace access and per-user settings are now decided on the stable user id rather than on the address someone happens to sign in with. Change your address and you keep your decks.
There are two privacy fixes in here worth reading, both on the analytics surface, both below.
Identity is the user id now
- Deck ownership compares user ids. The API carries
ownerIdas the identity and the client compares that, instead of matching address strings. - A workspace grant is decided against the actor’s own organization, not inferred.
- Per-user settings are keyed on the user id, so they survive an address change.
- The migration verifies and backfills itself, so an existing instance ends up with every stamp filled rather than half converted. After upgrading,
node scripts/verify-identity-migration.jsconfirms it landed on every row. - Version snapshots no longer carry owner e-mail addresses, and a migration wipes them from the rows you already have.
Analytics
- Anonymous viewers can erase their own data. Someone who viewed a deck without an account can now request erasure of their own session, proving possession of the session token rather than needing an account. If you present to audiences who never sign in, this is the surface that answers a GDPR request from one of them.
- The internal/external tracking toggle is gone.
analytics.enabledis the only switch; the second toggle promised a distinction the data never carried. If you had tracking off through that second toggle, you now have to turnanalytics.enabledoff, or you will start collecting again. - The session list no longer returns the session token. It was in the response and had no reader.
- A per-deck device label replaces the raw device id. The dashboard shows “this is the same visitor” without the identifier that would let two decks be correlated.
Presenting
- The join link can edit the session’s speaker notes. Someone holding the join link can now write notes during the session, rather than only reading them.
Fixed
- A failed collaborator invite answers 500, not 400. A storage failure was reported as though you had sent something wrong.
DATABASE_URLwins over the individualDATABASE_*variables indb:migrate, matching what the server itself does. If you set both, migrations used to run against a different database than the app.- Queued exports load their theme properly, so a background export is styled the way the interactive one is.
- An invite failure branches on the error code, not on the message text.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.17.0 release on GitHub.
- Deck ownership compares user ids. The API carries
-
v1.16.0
People start being identified by a stable user id rather than by their e-mail address, and the last audience data leaves the disk.
A quiet release with one long-running theme underneath it: who someone is stops being their e-mail address. Deckyard used the address as the identity in ownership, collaborator rows and audit stamps, which meant that changing your address changed who you were. This release adds the stable user id alongside it; the releases that follow move each surface over to it.
Identity gets a stable key
- Decks carry an owner, creator and last editor as user ids, next to the addresses they already stored. Nothing changes for you yet; the reads switch over in 1.17.0.
- Collaborator rows carry a user id alongside the address.
- An ownership transfer persists the new owner rather than leaving the stamp behind.
The rest of the audience data moves to Postgres
- Questions, interactions and feedback are database rows. With 1.15.0 having removed the file backend, this closes the last path that wrote audience input to disk.
Fixed
- Collaborator rows are scoped to the deck, not to the session. The wrong scope could show a collaborator list that belonged to a different context.
- A failing language switch says so. Switching a deck’s language silently did nothing when the switch failed; it now reports the failure.
Security
- The public share-link password check is rate-limited. A password-protected link could be attacked at whatever rate the network allowed. If you publish password-protected links, this is the reason to take this update.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.16.0 release on GitHub.
-
v1.15.0
PostgreSQL becomes the only storage backend. An instance that still keeps its decks as JSON on disk has to import them before it will boot.
Deckyard used to be able to store everything as JSON files in a data directory, with Postgres as an option. That option is now the only one: the file backend is removed, and every deck, setting, live session and e-mail template lives in the database.
This is the release that needs a plan before you update. If your instance has been running on file storage, it will refuse to boot after this update rather than present you with an empty workspace next to data it can no longer read. The way through is one command, and it is below.
Import before you restart
The boot check counts the decks in your data directory. If it finds them and the database is empty, it stops and prints both ways out instead of starting.
- Run
npm run db:import. It reads the old data directory and writes it into Postgres. The file data is only read, never moved or deleted, so the import can be repeated and the old directory stays as your fallback. - Take a database backup and keep the data directory until you have opened a few decks and confirmed they look right.
STORAGE_MODEnow has one spelling per backend.postgresqlused to be a silent alias forpostgres, and an unknown value used to fall through to file storage. Both are boot errors now, so you can no longer end up on a backend you did not ask for.
Postgres ships with the stack
- The compose stack includes a Postgres service, so a fresh install needs no separate database setup.
.env.exampledocuments the database block as ordinary configuration rather than as an opt-in. - The installer fails fast on a stale
DATABASE_HOSTinstead of starting and failing later.
What moved into the database
Four things that were still JSON on disk are now rows, which is what makes a multi-instance or containerised deployment behave:
- Questions, interactions and audience feedback.
- Live sessions and follow codes.
- Instance settings, so an admin change no longer depends on a writable config file.
- E-mail templates.
Sandbox TTL cleanup and quota accounting run on Postgres too. The compose sandbox stack is the one exception: it has no database service and still reads deck JSON from disk, so it pins
STORAGE_MODE=fileexplicitly.Also in this release
- The interface is offered in twelve languages, of which Dutch and English are the two that are gated on completeness; the other ten are best-effort and fall back to English where a string is missing.
- Image usage is found in the database, so the image library no longer reports an image as unused because it was looking on disk.
- Seven slide-type CSS classes are no longer emitted:
team-cards-group-left,kpi-note,quote-author-text,poll-results-main,sfi-header,sfi-card-code,sfi-card-qr. No built-in theme targeted them, so Deckyard’s own themes are unaffected. If you maintain a fork that styled one of these names, move those rules to the surviving class on the same element. - The numbering stays in 1.x. A 2.0.0 was briefly prepared and reverted; Deckyard’s beta stance caps breaking changes at a minor for now, which is why a release like this one is not a major.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.15.0 release on GitHub.
- Run
-
v1.14.0
The last two libraries the app fetched from a CDN are now served by your own instance, and themes get a first set of role tokens.
The release before this one took DOMPurify off a CDN. This one finishes the job for the two remaining third-party scripts, so an instance behind a firewall or on an air-gapped network no longer depends on somebody else’s server to highlight a code block or set a formula.
Syntax highlighting and formulas come from your instance
Prism (code highlighting) and KaTeX (mathematical notation) were loaded from a public CDN at page load. They are now vendored into the repository and served from your own instance, loaded lazily by the app shell rather than on every page.
- An offline or CDN-blocked install renders code and formulas correctly, where before it fell back to unstyled text.
- They load only when a deck needs them. Nothing is fetched for a deck that contains neither a code block nor a formula.
Themes get role tokens
The first phase of a longer piece of work on the theme system: slides start reading tokens that name a role (a surface, an accent, text on a dark plane) instead of naming a slide type. Nothing changes visually and no existing theme breaks; it is the groundwork for the consolidation that lands in 1.19.0.
docs/reference/slide-roles.mddocuments the vocabulary, if you write your own themes and want to see where this is heading.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.14.0 release on GitHub.
-
v1.13.0
Deckyard stops asking a CDN for its sanitizer, the bundled fonts turn out to have been the wrong ones all along, and there is now an image source with no licence attached.
Two of the three things in this release are the same thing: an instance should not need anybody else’s server to work correctly. The sanitizer came off a CDN, and the fonts that were meant to be self-hosted turned out to be re-downloaded from Google on every install, without a pin, and with the wrong bytes.
This is a reason to update. If your instance blocks third-party requests, or sits behind a firewall that quietly failed to fetch
cdn.jsdelivr.net, the editor’s sanitizer was not loading at all. And separately: every deck has almost certainly been rendering in a fallback typeface rather than the one its theme names. Both are below.The sanitizer is served by your own instance
The app shell loaded DOMPurify from
cdn.jsdelivr.net, which made a third party a dependency of two things at once: the security posture, and whether the app booted at all. It is now vendored from the npm copy that the server-side sanitizer already used, pinned by the integrity hash inpackage-lock.json.- An air-gapped or CDN-blocked install gets a working sanitizer, where before it got whatever the browser did when the script failed to load.
- The browser and the server now run the same DOMPurify. The CDN tag was stuck on 3.2.2 while the server had moved to 3.4.12.
The fonts were the wrong bytes
This one is worth reading even though it is filed as a fix. Google serves each font family split into per-script subsets and expects the browser to pick between them by
unicode-range. The installer took whichever@font-facecame back first, which wascyrillic-ext. So every curated font on disk contained no Latin glyphs at all, and everything that asked for one, slides, exports, the theme picker and the app’s own interface, silently rendered in a fallback face instead. All four Inter weights were byte-identical to the same Cyrillic file.- Each weight now ships as
latinpluslatin-ext, with a realunicode-rangeon every generated@font-face. Your decks will start rendering in the typeface the theme actually names, which for most installs is a visible change. - The download is pinned.
scripts/google-fonts.lock.jsonrecords the exact URL, SHA-256 and byte size of every file, and the installer refuses anything that does not match. Until now the contents ofassets/fonts/google/were a function of Google’s release schedule rather than of the release you installed. - Exports lose about two thirds of their font weight. Most curated families are variable fonts where every weight resolves to the same file, but each weight was declared separately, and an export base64-inlines every rule. The default theme was shipping roughly 930 KB of fonts where about 253 KB is unique, in every standalone HTML file and every PDF or PNG render.
- Two built-in themes were exporting a fallback face where their CSS asked for a weight the theme did not list: Corporate was missing Archivo 500, Midnight was missing JetBrains Mono 500 and 700. All six themes are regenerated, and a test now fails if a list drifts again.
- Font family names are escaped properly on the way into CSS. A name ending in a backslash could escape the closing quote of the declaration it sat in, and family names reach that path from uploaded fonts and custom theme JSON, so this was reachable input rather than a lint. Five places were interpolating names and URLs into CSS strings, and three of them were doing no escaping at all.
An image source with no licence attached
There is a third image source in the picker: 30 abstract gradients rendered from the six built-in themes’ own palettes. No API key, no attribution, no rate limit, no external request.
The reason it exists is the licence question rather than the aesthetics. Stock images come from a third-party API with terms attached, which is awkward on a public or guest-facing instance where uploads are off and stock is the only source someone has.
- The toggle is
stockMedia.bundled.enabled, and it is off by default. There is an admin checkbox for it. Nothing changes on an instance that leaves it alone. - A picked gradient survives export. The files sit under
assets/gradients/, which is the prefix the exporter inlines, so it works in PDF and PNG exports, published pages and embeds with no special handling.
The built-in slide-type count is unchanged at 34.
For the complete, commit-level list, see the 1.13.0 release on GitHub.
-
v1.12.1
Two deprecated slide types leave the registry, a page without video stops loading a video player, and links stay readable on dark theme variants.
A small release on the same day as 1.12.0, with one clean-up worth reading about: the two slide types that had been deprecated for a while are now actually gone. Nothing breaks, and there is nothing to run.
Two slide types are retired
content-columns-slideandcard-stack-slideare out of the built-in set. The built-in slide-type count goes from 36 to 34.- Stored decks do not break. A slide of either type renders as an archived placeholder with all of its content still visible, so nothing is lost and nothing crashes.
- Card stacks have a successor. The placeholder names
icon-card-gridas the rebuild target; it takes the sameitems[]shape, andscripts/migrate-slides.jswill do the conversion for you if you want it done in bulk. - Content columns has no successor. Rebuild those slides with whichever type fits; there is no automatic conversion, because there is no type that means the same thing.
A page without video makes no third-party requests
Bunny’s
player.jswas loaded on every page of the live app, whether or not a deck contained a video. It is lazy-loaded now, and only when a video slide is actually present. For a deck without video that is zero third-party requests, which is both a privacy improvement and a faster first load.Links stay readable on theme variants
A theme variant with its own slide background inherited the base link colour, which could fall away against that background. The link colour is now derived per variant, so an inverted or dark variant gets a readable link without a manual override.
Upgrade notes
- Removing deprecated slide types in a 1.x release is the intended shape. Deckyard stays on 1.x for the whole beta, so a retirement like this one is not held back for a major version.
- Nothing to run. The migration script is optional and only exists for people who would rather convert their card stacks than leave them as placeholders.
For the complete, commit-level list, see the 1.12.1 release on GitHub.
-
v1.12.0
The slide editor was rebuilt on one generic engine, so every type edits the same way, and `?lang=` becomes the only way a URL names a language.
Almost every hand-built slide form in the editor is gone, replaced by one editor that reads each type’s own declaration. You will notice it as consistency rather than as a new feature: adding, removing and reordering items works identically on every slide type now, because it is literally the same code.
If you share links carrying
?locale=, they need to become?lang=. The old alias is removed in this release; see the URL section below.The editor got rebuilt
The per-type forms were the oldest part of the editor and the least consistent: seven different collection forms, each with its own idea of what an add button looks like.
- One generic collection editor replaces all seven. Lists, cards, matrix and the rest now add, remove and reorder the same way.
- Charts and tables become a closed per-field vocabulary. Each field is a declared control instead of a bespoke form, which is why they now behave like the rest of the editor.
- The image forms became declarations too, sharing one element card.
- New items get defaults in the deck’s language. Per-item defaults resolve per deck language, so adding a row to a Dutch deck no longer seeds it with English placeholder text.
- The slide list reads each type’s
labelField, so the label in the left-hand list is the field that actually identifies the slide.
A URL that names a language is obeyed
?lang=is now the only UI-locale URL parameter. The?locale=alias is removed. Nothing inside the app used it, but a link you sent someone might: those links need updating, or they open in the recipient’s own saved language rather than the one you meant.?lang=outranks the saved interface language for that session, so a link that names a language opens in it, even for someone who normally works in another.
Fixed
- Quote slides stop jumping while you type. The editor canvas reflowed when the text field took focus, so the text moved under the cursor as you clicked into it. A delegated control on the same slide also stopped rendering as if it were broken.
For self-hosters
- The retention cleanups are actually scheduled now. All three ran only if something called them, and nothing did. They are wired into a daily job, honouring
ACTIVITY_RETENTION_DAYS(default 180). If you have been running an instance for a while, expect activity data older than the retention window to disappear on the first nightly run after upgrading, which is what the setting was always supposed to do.
The built-in slide-type count is unchanged at 36.
For the complete, commit-level list, see the 1.12.0 release on GitHub.
-
v1.11.0
A slide type now has exactly one spelling on the wire, the write API refuses types it does not know, and PDF export stops producing files nobody wanted to send.
This release settles something the format had been vague about: what a slide’s
typeis actually called. Until now the same type could reach you under two spellings depending on which door it came out of, and a write could put any string at all into that field. Both are fixed here. Around that, organizations became usable for the people in them, and PDF export got a round of repairs.If anything you run reads
slides[].typeoff an export or an API response, read the first section before updating. Stored decks migrate themselves, so an instance needs nothing; an integration that pattern-matches on the old spelling does.One spelling for a slide’s type
Export and every read API now write the canonical reverse-DNS id,
eu.deckyard.slide.title, and nothing else. Before, the bare registry key could also appear on the wire, so a consumer had to know both spellings and guess which one it was looking at.- Anything reading the bare key has to move. There is one spelling now, and it is the reverse-DNS one.
- The
slideTypesmanifest is gone from the deck envelope. It described the types used in the deck, was read by nothing, and duplicated a fact already on every slide.
The write API validates the slide type
An unknown
slides[].typeis a400on every write path. On Postgres installs any string used to pass through silently, which meant a typo in an integration produced a deck full of slides that could never render, and nothing said so until somebody opened it.If you write decks over the API, an unknown type now fails at the point of writing rather than at the point of reading.
Stored decks migrate to schema v4 by themselves
Decks already on disk carrying an old type spelling are folded to the canonical form on the next read or write. There is no script to run and no downtime; an administrator does not have to do anything for this one.
Organizations: inviting people, and a profile screen
1.10.0 gave multi-organization installs their first screens and left two obvious holes. Both are filled here.
- Invite people into an organization from the interface, with the invitations that actually went out reported back to you.
- Members can reach the member list, not just admins, so an ordinary member can see who else is in the organization.
- An organization profile screen, including delete for the owner.
- Administering the organization you are currently in behaves correctly, which it did not always do when you belonged to several.
PDF export gets a round of repairs
Five separate ways a PDF could come out wrong, all fixed:
- Images are capped at their display size instead of their source resolution, which is most of the reason exported PDFs were as large as they were.
- Gradient slide backgrounds are rasterized, and so are gradient pseudo-element layers, so a gradient no longer disappears or bands in the PDF.
- Blurred shadows stop reaching the PDF as luminosity masks, which is what turned a soft shadow into a grey rectangle.
- The slide font is anchored to the theme, so the app’s own interface font stops leaking into the exported file.
- Local
url()assets are inlined, and an image that cannot be fetched is blanked rather than left as a broken box.
Dutch field labels for eight slide types
Comparison, timeline, countdown, custom HTML, embed, lead capture, matrix and process slides had English field labels inside a Dutch interface. They are translated now, so a Dutch editing session no longer switches language halfway down the form.
For implementors
The spec work from this cycle is visible in the reference documentation: the published JSON Schema is open and the
structurefacet is normative, type ids are reverse-DNS with-slidedropped, and the catalogue now carries three tiers plus afallbackfacet that says what a reader does with a type it has never seen.The built-in slide-type count is unchanged at 36.
Upgrade notes
- A breaking change in a minor version is the intended shape. Deckyard stays on 1.x for the whole beta; 2.0.0 is reserved for leaving beta, not for a cleanup that happens to break something. The single-spelling change and the stricter write validation are exactly that kind of cleanup.
- Nothing to run. Stored decks migrate on access; the work is in your integrations, if you have any.
For the complete, commit-level list, see the 1.11.0 release on GitHub.
-
v1.10.0
Organizations get a real interface, an exported deck loses about half its weight, and one slide type is retired - with the rename handled for you on upgrade.
The headline of this release is not on a slide: an install that runs more than one organization finally has an interface for it. Around that, exported decks got dramatically lighter, and a slide type that was only ever a second name for another one is gone.
If you run on the file store, the upgrade has one manual step. Stored decks are renamed automatically by database migration
056, but a file-store install runsnode scripts/migrate-lijstje-slide.jsonce. Details below.Organizations get an interface
Multi-organization installs have had the data model for a while and no way to see it. This release adds the screens.
- An organization switcher in the user menu, so you can tell which organization you are working in and move between them.
- Admin screens now follow your role in the active organization, not the instance-wide admin flag. Someone who is an admin in organization A and an ordinary member in B no longer sees admin screens while working in B.
- The Users tab becomes the member list of the active organization in a multi-org install, and it is now a place where you do things: change a member’s role, remove someone, leave yourself, transfer ownership. The list is paged, so a large organization is workable.
- Several permission checks around organization ownership and settings were tightened along the way.
Still missing, so do not plan around it: inviting people from the interface, and the organization profile screen. Multi-organization remains in development.
One slide type fewer, and the rename is done for you
lijstje-slideis retired. It was a second name forlist-slideand never a second type, which meant the same slide could be stored under either name depending on how it was created.- Stored decks are renamed automatically when you upgrade, by database migration
056. - File-store installs run
node scripts/migrate-lijstje-slide.jsonce, after upgrading. - Anything that slips through both renders as an archived slide pointing at
list-slide, not as a broken slide.
split-partner-title-slidealso leaves the registry in this release. It has been archived for several versions, so it was already unavailable when building a deck; slides that still carry it degrade to archived slides. Between the two, the built-in slide-type count goes from 38 to 36.An exported deck stops carrying the editor
A standalone HTML export shipped the editor’s entire stylesheet: modals, inspectors, the slide-type picker, settings, none of which an exported deck can use. The export now ships viewer CSS only. That takes roughly 92% off the stylesheet inside the file, which on a real deck is about half its total weight.
Nothing about the export changes visually, and
?ui=minstill collapses the chrome the same way.A green and brass default theme
There is a new built-in theme, Forest: forest green with brass, warm paper and sage surfaces, and a chart ramp that alternates hue and lightness so two adjacent categories differ by more than color. It is now the default theme.
Existing decks are unaffected. A deck stores its theme id, so only new decks and decks explicitly set to
defaultmove. The violetdeckyardtheme keeps its id and its name; it is one style among six now rather than the one everybody inherits.Smaller changes
- Video slides: the watch link shown in exports is yours to set. A video cannot play inside a PDF, so the export shows a link instead; you can now say where it points.
- KPI tiles take their colors from the theme through tokens, instead of carrying their own.
- A video slide’s poster image now renders in the PDF and PNG placeholders.
- AI suggestions in comments are recognized by the effective author, so a suggestion made on someone’s behalf is labeled correctly.
For self-hosters
- Maintenance mode for deploys. Putting the instance into maintenance during a deploy means a save that lands mid-deploy fails visibly instead of silently, which is the difference between a retry and lost work.
- A partial write no longer erases deck columns. Writing part of a deck could blank fields it did not touch.
- The published JSON Schema no longer carries legacy fields, so a second implementation validating against it is not told to support fields Deckyard has dropped.
- The editor got quieter. Deck thumbnails are rendered when you save rather than when you look, and the slide list no longer rebuilds itself every time a lock or comment event arrives.
For the complete, commit-level list, see the 1.10.0 release on GitHub.
-
v1.9.0
Deck thumbnails stop turning back into placeholders after every edit, a table can carry one colour across its whole top row, and AI refine accepts the new text-blocks shape.
The second release of the day, after 1.8.0, and the one worth updating for is the fix at the top: the deck overview stops flickering back to grey every time you open a deck.
Thumbnails stop reloading after every edit
The card for a deck on the home screen shows a picture of its first slide. That picture was cached against the deck’s revision number, which changes on every save - so editing slide nine, or just opening a deck and closing it again, threw the picture away. And a thrown-away picture costs about ten seconds of shimmering placeholder before the card fills in again.
The cache now keys on what the picture is actually made of: slide 1 and the resolved theme. An edit anywhere else in the deck no longer invalidates anything. While a new picture is being rendered the previous one is served, so a card is never blank just because slide 1 is one edit stale.
One colour across a table’s top row
The top-left corner cell of a table has always taken the colour of the label column, so a
softtable shows a faintly tinted corner set slightly apart from its header. That is a legitimate look, and now it is a choice: a new Top-left cell setting picks whether the corner belongs to the label column (as before) or to the header row, so the whole top row reads as one band.It composes with every table style rather than being a fourth style, so your existing table keeps whatever style it has. The difference is visible on
soft; onplainandpanelit changes nothing, because their header and first column already share a colour.AI refine accepts four-row text blocks
Since text blocks became an array (
rows[]), the schema behind AI refine still demanded the older numbered fields, so it rejected every array-shaped text-blocks slide - including freshly created ones, and any slide using 1.8.0’s fourth row. Refine now validates the array shape, with the numbered fields optional and passed through untouched.For self-hosters
server/data/deck-thumbs/gets cleaned up. It grew by one file per edit to a first slide and nothing ever pruned it. Older rasters for a deck are now removed once the fresh one lands.- A 500 in the log when two people opened the same slide is gone. Acquiring a slide lock is now atomic; the editor always handled the collision quietly, so this was never visible in the interface, only in the server log.
For the complete, commit-level list, see the 1.9.0 release on GitHub.
-
v1.8.0
A list slide keeps the text size you chose and fills the slide, text blocks take a fourth row, and quote slides stop typing themselves out.
Three changes to how slides look while you are reading them, none of which asks anything of you. Existing decks are not migrated and nothing breaks; two of the three simply make slides you already have look the way you meant them to.
A list slide keeps the size you picked
Choosing “Large” on a list of seven short bullets did nothing: the slide rendered at the default size, in two columns, with the bottom half empty. Two separate causes, both now fixed.
- An explicit text size wins. Past six items the size was thrown away outright, on the assumption that the list was still one column. At seven items it is two, where roughly twice as much text fits. Size and column count are now resolved together against a measured capacity table. The one case that still steps down - a list long and wordy enough to spill across two columns - says so, in the inspector under “Text size”, instead of silently changing nothing you can see.
- The rows spend the leftover height. They used to stack against the top of the list box and leave the rest of the slide blank. They now grow into it, so the dividers spread evenly across the slide rather than crowding into its top half.
Measured across 1512 combinations of item count, title length, text length, subheading, columns and text size: nothing overflows, 628 of them are fuller, and none is emptier.
Decks imported from markdown benefit too.
lijstje-slide, which the importer still emits, was a full copy of the list-slide definition that had drifted to a much older layout; it is now a real alias, so the same content renders the same either way.Text blocks take a fourth row
The cap on a text-blocks slide goes from three rows to four. Nothing changes for a slide with three or fewer - those render byte-for-byte as before - and the layout tightens a notch at four rows so the extra one fits without riding over the title.
The fourth row lives only in the canonical
rows[]shape, which is what the editor writes. The older numbered fields (row1,row2,row3) stay frozen at three on purpose, so hand-written or imported content keeps its meaning.Quote slides stop typing themselves out
A quote slide typed its quote out character by character, and it did so regardless of the deck’s “Builds” setting: turning builds off never reached this one effect. It also jittered, because the quote block centres the whole group - quote, byline and portraits - so the block shifted while the text grew.
The effect is removed rather than adjusted; it can come back properly later. The per-bullet typewriter is a different effect and still works: the “Typewriter” reveal style in the deck settings is untouched.
For the complete, commit-level list, see the 1.8.0 release on GitHub.
-
v1.7.1
One fix: the steps on a horizontal process slide line up along their top edge again.
A single fix, released the same day as 1.7.0. Nothing else changed, and there is nothing to do beyond updating. The reason to be on 1.7.x at all is in 1.7.0, where PPTX export stopped being silently wrong after a PDF import.
Process slides stop looking crooked
On a horizontal process slide, every step was centred vertically against the tallest one, so a step with a shorter description sank as a whole: number, title and all. Descriptions are rarely the same length, so the row usually looked misaligned by a few pixels in a way that was hard to name and easy to see.
The steps now share a top edge, while the group as a whole stays centred in the slide, so a deck that already looked right does not suddenly shift upward. Checked across multiple rows as well, at six steps.
Existing decks simply look better after the update.
For the complete, commit-level list, see the 1.7.1 release on GitHub.
-
v1.7.0
The field list an agent is handed now comes from the code that defines it, PPTX export stops breaking after a PDF import, and the format sentinel takes the product name.
Three changes: one that repairs what an AI writes into a slide, one that repairs a file it turned out you could not trust, and one rename that asks nothing of you.
If you use PPTX export and file import on the same instance, this is a reason to update now. On 1.6.0 and earlier, exporting a PowerPoint file after somebody had imported a PDF produced a deck with rubbish where the slides should be, and it did so without an error. Details below.
The catalogue agents read is generated, not maintained
A slide type declares its fields once, in the registry. The list handed to a model over MCP was a second copy of that, kept by hand, and it had drifted in five places. Two of them were visibly broken:
video-slidewas offered a field that does not exist. Agents were told to fillvideoUrl; the type readssource. So every AI-written video slide fell back, silently, to the demo video.payoff-slidewas offered ataglinethat the type never reads at all.
That catalogue is now derived from the registry, so this class of mistake cannot be introduced again. The same change also tells agents about fields an author could always fill in by hand but the catalogue had never mentioned.
PPTX export was silently wrong after a PDF import
Both exports render through headless Chrome, and after a PDF import had run in the same server process, Chrome started handing back a different kind of bytes. The PPTX builder took them at face value, so the file it wrote had nonsense where the slides should be, with no error anywhere in the chain.
Restarting the server cleared it, which is exactly what lets a bug like this survive: it looks like a fluke, and the next export works. It is fixed, and there is now a smoke test that starts a real browser, so a silent regression of this shape fails in CI rather than in somebody’s deck.
The format sentinel says
deckyard.deckThe
formatfield inside a portable deck, and the media type of a.deckpackage, used to sayslidecreator- a placeholder from December 2025, written before this product had its name. They now saydeckyard.deckandapplication/vnd.deckyard.deck.Nothing to do, and nothing breaks. The old values are accepted on read for good, so a deck exported by an older version still opens. The file extension is unchanged: a package still downloads as
<title>.deck. The one thing worth a look is tooling of your own that matches on theformatfield, which should accept both values.The format spec has been updated to match, and names both.
For the complete, commit-level list, see the 1.7.0 release on GitHub.
-
v1.6.0
A published deck stops requesting things it does not use, and an embedded one can now drop the presenter chrome around it.
Two changes, both about the page you hand to somebody else. A published deck used to load syntax highlighting, math typesetting and a video player whether or not it contained code, math or video; now it loads them only when it does. And a deck in an iframe can be asked for a minimal interface, so the frame is the slide and nothing else.
Fifteen requests to third parties, down to none
Measured on a real nine-slide deck with no code blocks, no math and no video: fifteen requests to third parties before, none now. Fourteen of them went to jsDelivr for Prism (the core, ten hard-coded language packs and a theme stylesheet) plus KaTeX, and one to mediadelivery.net for the Bunny player.
Three things are worth being exact about:
- This is every published page, not just the HTML download.
/p/, the embed route and the exported file are built by the same code, so an operator sharing a link gets the same benefit as one sending a file. - The requests became conditional, not removed. A deck with a code block still loads Prism from jsDelivr, now the core plus the languages that deck actually uses instead of ten fixed ones. Math still loads KaTeX; a Bunny video still loads its player. A deck with a code block and a formula makes five requests.
- Nothing leaked and nothing was repaired. Requests were being made that were not needed. That is a different thing from a vulnerability, and it should not be filed as one.
Why it is the headline rather than a performance footnote: Deckyard is self-hosted software that people run to keep their data in their own hands, and jsDelivr runs on Fastly in the United States. A deck that phoned out to two American CDNs to display nine slides of plain text was an awkward thing to hand a public institution.
?ui=minfor embedded decksAdd
?ui=minto any published or exported deck URL and the title bar and control row disappear, with their layout rows collapsing to zero height. The scaled 1600x900 stage is then the whole frame, which means a host page can size an iframe with a plainaspect-ratio: 16 / 9instead of compensating for a fixed chrome height.- The 3px progress bar stays. It is absolutely positioned, so it costs no layout height and the frame remains exactly 16:9.
- The visible slide counter goes, because it was the part that wrapped on a narrow frame and the host page can show its own. Screen readers still hear “Slide 3 of 9” and the slide title.
- Arrows, space, Home, End and
Ffor fullscreen all keep working. - Without the parameter nothing changes, so existing links are untouched.
The name and meaning match the
uioption the embed API already had, so the two runtimes keep one vocabulary.Along the way, the reference documentation for the standalone export gained a section on URL parameters, which it did not have at all:
loop,autoplayandintervalexisted but were written down nowhere.For the complete, commit-level list, see the 1.6.0 release on GitHub.
- This is every published page, not just the HTML download.
-
v1.5.1
Dependency maintenance, with one thing to check first: the practical Node floor moved to 22.13.
A maintenance release: seven dependency updates and the one code change one of them required. Nothing new, and nothing behaves differently. There is one thing worth doing before you update, and it is not in the release itself.
Check your Node version first. Deckyard has asked for Node 22 for a while and still does, but two dependencies in this release raised their own floor:
jsdom(25 to 29) now wants 22.13 or newer, andpuppeteer-core(24 to 25) wants 22.12. On Node 22.0 through 22.12 you were fine on 1.5.0 and are now under both. npm does not enforce that by default, so it installs with a warning rather than an error, and the parts that would notice are HTML export and sanitisation. That is the kind of thing you find out three weeks later, which is why it is here.What else is in it
- zod moved to 4, and the AI schema validators moved with it: zod 4 dropped
ZodError.errorsand the five validators read that field. Both sit in this release, so nothing about AI generation changes between 1.5.0 and 1.5.1. - Chrome 150 under the export, via
puppeteer-core25. Verified by hand on a real render, because no test starts a browser; the output is unchanged. - jsdom 25 to 29, four majors under the HTML sanitizer. Twenty-one XSS and mXSS vectors through the four sanitise functions produce byte-identical output on both versions. Nothing was repaired here; it was checked that nothing changed.
- The RSS, Atom and JSON feeds are unchanged after
feed5 to 6, plus a group of eight minor and patch updates and two CI actions that self-hosters never see.
For the complete, commit-level list, see the 1.5.1 release on GitHub.
- zod moved to 4, and the AI schema validators moved with it: zod 4 dropped
-
v1.5.0
Deckyard now picks the text color on a colored surface by measuring contrast instead of guessing from brightness, and the theme editor shows you the number while you choose.
Two commits, and one of them changes how decks you already have look. Deckyard puts light or dark text on a colored surface for you; that choice used to run on brightness and now runs on a measured contrast ratio, which flips the text color on nine of the twenty palette colors across the five built-in themes. Alongside it, the theme editor gained a contrast readout, so the number you were previously supposed to check elsewhere now sits next to the picker that produces it.
If you run Deckyard, read the upgrade note before you field the first “did something break?” message. Text that was white yesterday can be dark today, and that is the fix rather than a regression.
The automatic text color is now measured
Cards, accent blocks and background variants get their text color from Deckyard rather than from you. The old rule split on brightness: darker than the middle, use light text. That picks the color that looks logical, not the color that reads. Both candidates are now run through the contrast formula and the higher ratio wins.
Measured across all five built-in themes:
- Nine of the twenty palette colors get a different text color.
- Every flip lifts a failing pair to WCAG AA or better. No pair gets worse.
- Pairs under AA: nine, down to one.
- The sharpest case is
midnight’s icon block, which went from 2.05:1 - white on light blue, effectively invisible - to 8.16:1.
The one pair still under AA is
playful’s accent#ea580cat 4.40:1, where the accent color itself is the limit rather than the choice of text color.Because contrast is not symmetric around the middle of the range, the crossover sits near a relative luminance of 0.21, so mid-light backgrounds get dark text. That asymmetry is exactly why the old rule left so many pairs under AA.
The theme editor shows contrast while you choose
Every color pair in the editor now carries its contrast ratio and a WCAG 2.2 verdict: fail, AA or AAA, judged against the threshold for the text size in that role. Buttons and headings are measured as large text, body copy against the stricter one. An APCA Lc value sits beside it as a second reading.
- It reports, it does not block. A theme with a failing pair still saves. A self-hoster can have a brand reason to ship a low-contrast variant, and a settings panel is not the place to overrule that.
- The ratio is always shown, passing or not, so contrast is a property you can steer toward rather than an alarm that appears once you are already past the line.
- WCAG is the verdict; APCA stands next to it. WCAG 2.2 is what EN 301 549 and the European Accessibility Act reference, so it is the claim anyone can lean on. APCA, candidate work for WCAG 3, models perceived contrast better - particularly light text on dark, which is what
midnightis built on - but it never decides.
The readout covers the four main colors and the background variants. The brand palette carries no verdicts yet, deliberately: those tokens are about to be redefined, and badges on them would be obsolete on arrival.
Upgrade notes
- Existing decks will look different, and that is the point. Nothing to configure and nothing to migrate: the text color is derived at render time, so opening a deck is enough. Expect the change to be most visible on
midnightand anywhere an icon block or accent card sat on a mid-light color. If someone reports it as a bug, the pair they are looking at almost certainly measured under 4.5:1 before. - A custom theme benefits without being edited. The derivation is shared, so your own colors go through the same measurement. Open the theme editor to see where each pair now lands.
For the complete, commit-level list, see the 1.5.0 release on GitHub.
-
v1.4.0
A slide type can now carry the rules your organization set for filling it, and every registered type is either visible to an agent or withheld on purpose.
What an AI agent knew about a slide type used to come from a hand-written catalog inside Deckyard: editorial copy that says which type to pick. This release opens that up. Every registered type is now visible to agents, including the ones your own instance defines, and each type can carry the rules your organization set for filling it: which sources to draw on, which cut-off date applies, what has to be named. It is the first feature where a self-hoster fills something in, rather than something working better behind the scenes.
Two versions were tagged the same day. 1.4.1 followed half an hour later with a single fix to the streaming API, described under Upgrade notes. Install 1.4.1.
Your house rules travel with the type
- Every registered type reaches an agent, or is withheld on purpose. The
get_slide_typestool used to walk the hand-written catalog, so a type without an entry was invisible to agents and indistinguishable from one held back deliberately. It now reads the runtime registry, and types defined in your own builder UI arrive there too. Of the 38 registered types, 31 are offered and 7 carry an explicit opt-out; none are undocumented, and a test fails the build if that ever changes. - A type can carry its own usage rules. Alongside the copy that says what a type is for, a type can now hold the house rules for filling it in - the source a chart must come from, the reporting date that counts, the caveat that has to be stated. Those rules ship in every
get_slide_typesresponse, after the schema, so an agent reads the shape first and your rule second. - Four places to write them, depending on where the type lives. On a core type’s catalog entry; as an override in a fork, without patching the open-source file; on a file-defined type of your own; or in the builder UI for a type defined in the database. Rules are capped at 1000 characters per type, because they multiply by every visible type in every response.
A removed type keeps its content
The
freeform-slidetype is no longer registered. It had been deprecated for a while and was offered nowhere, so most instances have no such slides; if a stored deck does, nothing is lost. The content stays in the deck JSON, and the slide now renders a placeholder that names the type, says it was deliberately removed and points at the successor, instead of a bare “Unknown slide type” box.That placeholder is a general promise now, not a patch for one type. On canvas - editor, presenter, embed, PNG, PDF - it is bounded and says how many fields it withheld, because a 16:9 frame cannot grow. In the reader and reflow views it shows everything, since that is the recovery surface. Importing a deck with an unresolved type stores the original content as markdown rather than dropping it, which was the one path that actually destroyed anything. And the editor no longer crashes on such a deck: opening one used to produce a white error page instead of a placeholder.
A shorter inspector rail
The “INSPECTOR” title bar is gone; the cross that collapses the rail stays where it was. The background section is split by how often you touch it: colour is a plain visible field, while the image with its crop, focal point and overlay sits in a collapsed block that shows a thumbnail once a background is set. On a title slide the rail went from roughly 1310 to 376 pixels, which means Accessibility now fits on one screen without scrolling.
Fixes
#slide=<n>works again in exported HTML. An exported standalone deck ignored the deep link in the URL and always opened on slide 1, so sharing an export with a link to a particular slide did nothing. The pattern was emitted inside a template literal, where\dcollapses to a literald; it never matched, and had been broken for as long as the export runtime has existed.GET /api/v1/image-libraryanswered 500 on every call. A documented endpoint that had never worked: the route imported a function the storage layer does not export. It is wired up now, with the search, category and pagination parameters actually implemented and the OpenAPI description corrected to match the fields the library really has.- Writing tags answered 400.
PUT /api/presentations/:id/tagsandPOST /api/tagsread the request body one level too deep, so writing tags through the API was dead on arrival.
Upgrade notes
- SSE error events changed shape. This is the whole of 1.4.1. Server-sent events on the import, conversion and AI generation streams now carry their error text in
messageinstead oferror, and no longer include anokfield. Inside the app you will notice nothing; if you have a fork or an integration listening on those streams, this is one field name to rename. - A breaking change in a minor version is the intended shape. Deckyard stays on 1.x for the whole beta, so 2.0.0 is reserved for leaving beta rather than for a cleanup that happens to break something. The
freeform-slideremoval above is that change.
For the complete, commit-level list, see the 1.4.0 and 1.4.1 releases on GitHub.
- Every registered type reaches an agent, or is withheld on purpose. The
-
v1.3.0
Organizations become usable on a single instance, alignment becomes a property of the block, and a cross-user XSS in the slide list is closed.
The organizations track lands, and with it the answer to a question that had been half-built for months: how one Deckyard instance serves several organizations without either of them seeing the other. There is also a security fix that is a reason to update on its own.
Update if you run a multi-user instance. This release closes a DOM XSS in the slide list, described at the bottom of this page. A slide title is written by a person, so in a deck shared with colleagues the payload can come from someone else’s slide, and it fires when you drag one in the slide list. There is no configuration to change: updating is the fix.
Organizations on one instance
Three changes that only make sense together:
- Who you are no longer depends on where you are. Every authentication lookup used to filter on the organization stamped onto your account, while membership allows you to belong to several. Anyone whose home organization was not the one the request ran in resolved to nobody and got a 401. That is why
MULTI_WORKSPACE_ENABLEDwas never wired end to end: switching it on would have logged people out. Identity is now resolved on its own, and membership is a separate question. - A request runs in the organization your session resolved to. Switching workspace used to change the session cookie and nothing else, so reads, writes and lists all stayed in the default organization. The organization now travels with the request, and only ever one you are a verified member of: membership is re-checked per request, because a token outlives a revocation by up to two weeks.
- Authorization knows about organizations too. The permission layer granted workspace-wide reads and writes without mentioning organizations at all, and relied on the storage layer to scope the query. That held in practice, but a layer that only holds because the layer beneath it remembers to scope is not a layer. The four workspace grants now check that both sides are in the same organization; ownership, authorship and collaborator access are untouched.
Single-organization installations, which is most of them, behave exactly as before and pay nothing for this: the organization is answered from configuration without a database lookup, and no query is added anywhere.
Alignment belongs to the block
Aligning a title slide used to mean setting the title, subtitle and meta line each to “center”, which still left them centred on axes far apart, because each box sits somewhere different on the slide. Alignment is now a property of the visual block rather than of every field in it, set once through the layout switcher you already use. Fields that no longer own their own alignment say so instead of showing a control that does nothing.
Fixes
- Nothing outlives its view any more. A lifecycle audit of the client found six leaks of the same shape: something registered on mount that survived the unmount with no handle left to stop it. Pending reconnects kept reopening streams nothing could close, and cleanups registered after teardown vanished into a map nobody drained. The notification bell and both Q&A streams now reconnect with backoff instead of retrying every 1.2 seconds per viewer.
- A new deck starts in the language of your interface. Opening the app in English gave an English interface with the language toggle in “New presentation” still on the workspace default. A language you picked yourself still wins over both.
- A stalled thumbnail always resolves. A card in the deck grid whose thumbnail request never completed shimmered forever. Every card now reaches a real end state, and upgrades to the real image if it arrives later.
- Card stack follows the theme. The last-resort palette was a hardcoded purple belonging to one theme, so a custom theme rendered off-brand cards. It is now derived from the theme’s accent colour, with a neutral ramp as the final fallback.
- Optimistic-lock conflicts speak the standard error shape. Three presentation write routes returned a hand-rolled body when a save collided with someone else’s. They now emit the canonical envelope, with the stable
conflictandlockedcodes and the revision details preserved. Status codes are unchanged and the app is unaffected; if you built against the API directly, theerrorfield now carries a machine code and the human sentence moved tomessage.
Security
A cross-user XSS in the slide list. The drag image built when you pick up a slide interpolated the slide’s title straight into HTML. A slide titled with a fragment of markup would run it the moment someone dragged that slide, in the browser of whoever was dragging. Because slide titles are written by people and decks are shared, on a multi-user instance the code and the victim need not be the same person. The drag ghost now builds its label as text, and two neighbouring sinks in the JSON debug and import panels were rebuilt the same way.
For the complete, commit-level list, see the 1.3.0 release on GitHub.
- Who you are no longer depends on where you are. Every authentication lookup used to filter on the organization stamped onto your account, while membership allows you to belong to several. Anyone whose home organization was not the one the request ran in resolved to nobody and got a 401. That is why
-
v1.2.0
A small release: interface copy that reaches every locale, a translated editor toast, and a round of hardening for publicly reachable instances.
A quiet release between two bigger ones. Nothing changes in how you build a deck; the work sits in the interface language and in the parts of the stack that face the open internet.
Every locale gets the interface, not just English
Deckyard ships in 12 languages, but a label written straight into the code has no key to translate, so it rendered English everywhere without any test noticing. An audit found and translated the strings still on the main path: the slide-library toast, the theme card specimen, the data-source modal’s provider hints and field labels, the autoplay toggle, the focal-point control and the theme logo alt text. A new check in CI now fails the build on new hardcoded copy, so the list can only shrink.
The JSON debug modal’s “Invalid JSON” toast, the last raw string in an otherwise translated modal, now goes through the translation layer as well.
Hardening for instances open to the public
This round targets the anonymous sandbox, and it matters for any install where strangers can create decks:
- Rate limiting behind a reverse proxy. If Deckyard sits behind a proxy without being told so, the limiter keys every request on the proxy’s own address, so all visitors share one bucket and a single abusive guest trips the limit for everyone.
TRUST_PROXYandTRUSTED_PROXY_COUNTmake it key on the real client address instead. - A per-guest quota. Deck count and total bytes are now capped per guest at every point where a deck is minted (create, import, duplicate), with a smaller request-body cap bounding each one. A guest at the cap gets a clear 429 rather than filling the shared volume.
- One more SSRF hole closed. Remote images already went through the guard on export; a CSS
background-image: url()smuggled in through an imported custom-HTML slide did not. It is now inlined or blanked before the HTML reaches the renderer.
For the complete, commit-level list, see the 1.2.0 release on GitHub.
- Rate limiting behind a reverse proxy. If Deckyard sits behind a proxy without being told so, the limiter keys every request on the proxy’s own address, so all visitors share one bucket and a single abusive guest trips the limit for everyone.
-
v1.1.0
The first big release since going open source: real-time collaboration, a reworked editor, self-contained exports, and a hardened self-hosting story.
Deckyard 1.1 is the first substantial release since the code went public. It deepens the parts that make Deckyard a platform rather than a slide editor: people working in the same deck at the same time, exports you can hand to anyone, and a self-hosting path that stands up to real scrutiny.
Real-time collaboration
- Live presence: see who else is in a deck, with gliding name labels and focus chips on the slides they’re editing.
- Conflict-free co-editing built on CRDTs, so two people can type in the same deck without stepping on each other.
- Comments with @mentions and autocomplete, per-user read status, and GitHub-style subscriptions that follow you into the threads you join.
A sharper editor
- Selection-aware inspector that switches between element and slide settings as you work.
- Inline markdown editing straight on the canvas, with a floating toolbar.
- Draggable focal points on cropped images, fully keyboard-navigable.
- Reorder cards inline with overlay grip handles, and open image settings from a chip on the image itself.
- A spreadsheet-style data grid for chart slides, with a CSV toggle for pasting numbers in bulk.
Exports you own
- A unified export modal with color-coded formats, so the whole menu reads at a glance.
- Self-contained
.deckbundles that export and re-import with everything intact. - Standalone HTML with fonts embedded, so a deck renders offline with nothing to phone home for.
- A semantic, reflowable HTML reader export, plus smaller PDFs thanks to image compression before embedding.
Themes and branding
- Richer theme configuration stored on the theme itself, including background presets and title-slide layouts.
- Theme surface tokens now flow through the slide design system, so brand colors land consistently across slide types.
Self-hosting and security
- Single-IdP SSO over OIDC for self-hosted instances.
- A one-command installer with interactive
.envsetup, plus a non-interactive flow for agents and CI. - Security hardening across the board: rate limiting and brute-force throttling on auth, SSRF and CSRF protection, per-resource authorization for live sessions, multi-workspace isolation, and security headers.
Accessibility
- An HTML semantics pass across the render layer: landmarks, native lists, and RTL support.
- A document heading outline for visual export and embed, and an accessibility status panel with collapsible rules.
For the complete, commit-level list, see the 1.1.0 release on GitHub.
-
v1.0.0
Deckyard goes open source. The full platform - create, present, publish, collaborate - lands on GitHub under the MIT license.
The first public release. Deckyard was already a working platform in daily use; 1.0 is the moment its source became yours to read, run and change.
What shipped
- The full end-to-end platform: 35+ slide types, live presenting, one-click web publishing, and collaboration.
- Self-hosting from a single Docker Compose file, Node and Postgres.
- AI on your own terms: bring your own key, tune the prompts, or drive it over MCP.
- Export to PDF, PowerPoint, self-contained HTML, PNG and JSON, so nothing is locked in.
- MIT licensed, GDPR-native, built to run on infrastructure you control.