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.

  1. v1.97.5 Latest

    Safer video embeds

    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 play or embed URL 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.

  2. v1.97.4

    Safer bulk export downloads

    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.

  3. v1.97.3

    Complete export-dialog captures

    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.

  4. v1.97.2

    Added slides own their images

    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_slides and the public append-slides API, 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 write next to ai; a key with only ai causes no fetch.

    An image that cannot be copied leaves an empty field. So does any image URL the model writes itself. MCP append_slides and the public append-slides API now return warnings, where index counts 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.

  5. v1.97.1

    AI decks own their images

    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 write permission next to ai; a key with only ai causes 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_presentation and the public wizard API now report these in warnings, 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_CSS gate as a whole. Keeping the gradient while clearing its text colours selects the wrong pair.

    This release adds no database migration or configuration.

  6. v1.97.0

    Safer sandbox guest sessions

    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_SECRET of at least 32 characters before upgrading and disable COLLAB_ENABLED. For Docker Compose, put the secret in the .env file next to the compose files and restrict it to mode 600. You can generate a secret with openssl 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.

  7. v1.96.0

    Library comments and protected linked slides

    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.

  8. v1.95.0

    Library slides that know their source

    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 and docs/reference/publication-editions.md for 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_links table; it is additive and has no backfill. Using S3 storage, check that S3_PUBLIC_URL (or the derived bucket address) matches the base address your decks reference, then run node scripts/scan-edition-input-size.js before publishing: it now measures S3 objects too. The unused standalone embed player SDK has been removed; edition embeds keep working. If you use RENDERER_RUNNER_SOCKET, register this release’s renderer as described in docs/ops/renderer-runner.md.

  9. v1.94.0

    Editions that stay the same

    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 --apply and test the old links: they redirect to the first edition, even after you publish later versions. Then check obsolete previews with node scripts/sweep-published-previews.js --check before using --apply.

    Publish API calls now require expectedRevision and idempotencyKey; 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 that renderer-runner is green in the doctor before publishing. Keep older renderer registrations for existing editions. See docs/ops/renderer-runner.md and the 1.94.0 release on GitHub for the complete upgrade instructions.

  10. v1.93.1

    The right card

    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 in docs/ops/renderer-runner.md.

    See the 1.93.1 release on GitHub for the complete changes.

  11. v1.93.0

    One slide, or a few

    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 a 400 invalid naming the slides field. If you use the renderer runner (RENDERER_RUNNER_SOCKET), register the renderer of this release as described in docs/ops/renderer-runner.md.

    See the 1.93.0 release on GitHub for the complete changes.

  12. v1.92.1

    Every language, every width

    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_field in 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 in docs/ops/renderer-runner.md.

    See the 1.92.1 release on GitHub for the complete changes.

  13. v1.92.0

    Review what the AI made

    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 (POST and PUT /presentations, POST /presentations/:id/slides) accept a reviewNote per 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 a reviewUrl that opens the review; that link never offers to discard the deck. The public API refuses raw _ai* keys with 400; a PUT without reviewNote keeps the stored explanation.

    Fixes

    Picking an image from ImageKit no longer makes an image slide impossible to save; migration 102 keeps imagekitFileId on 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, and node scripts/doctor.js reports the runner. Setup is in docs/ops/renderer-runner.md.

    See the 1.92.0 release on GitHub for the complete changes.

  14. v1.91.1

    Keep writing through validation errors

    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.

  15. v1.91.0

    Consistent slide fields

    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 through propertyNames. 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 tagline and per-slide content.videoOverride are 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_use with 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.

  16. v1.90.0

    Mail transport choices

    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, retain BREVO_API_KEY, rename BREVO_SENDER_EMAIL to MAIL_FROM, and rename BREVO_SENDER_NAME to MAIL_FROM_NAME. The previous names no longer work. Stored instance sender settings remain supported and take precedence over environment settings.

    Choose MAIL_TRANSPORT=smtp, scaleway or mailgun with the corresponding provider settings, or MAIL_TRANSPORT=none to 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.js must switch to server/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.

  17. v1.89.1

    Saving and importing fixes

    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.

  18. v1.89.0

    Administration fits smaller screens

    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 escapeHtml from shared/slide-types/helpers.js; its re-export from server/utils/html-utils.js is removed.
    • Import themeVarsCssText from server/utils/theme-vars-css.js; its re-export from server/utils/themes.js is 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.

  19. v1.88.0

    Revoked access also closes analytics

    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 limit and cursor, then read the named list and nextCursor. offset is refused and total is gone. The dashboard list also refuses sort; rankings remain in the dashboard’s top presentations. Member counts have their own GET /api/organizations/:id/members/count endpoint. 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.js or its concern modules, getViewSessionsForPresentation becomes listViewSessions, and getPresentationsWithAnalytics becomes listDashboardPresentations. ANALYTICS_LIST_SORTS is removed.
    • Those functions, listAnalyticsReports, listOrganizationMembers and listUsers require a page argument and return { items, next }.
    • parsePaginationParams is removed from server/utils/request-validators.js. Use parseListPage, or parseListLimit for a bounded search, from server/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.

  20. v1.87.0

    PowerPoint becomes editable slides

    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 .pptx directly: 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 .pptx on 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}/comments and the MCP tools list_comments and list_recent_comments move onto the page contract the other lists got in 1.85.0 and 1.86.0. Send limit and, for the next page, the cursor you got back; the response is { comments, nextCursor }, newest first, and nextCursor is null on the last page. On a deck a page counts threads, each with all its replies.

    • total is gone, and so is offset: a request with offset is refused with 400 and a hint to use cursor.
    • limit must be between 1 and 100. For the MCP tools that was 200.

    An integration that read total or stepped through comments with offset needs 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.

  21. v1.86.0

    Every list pages from the server

    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-library and GET /api/v1/image-library

    Both library endpoints move onto the page contract that GET /api/v1/presentations got in 1.85.0. Send limit and, for the next page, the cursor you got back; the response is { items, nextCursor } for the slide library and { images, nextCursor } for the image library, and nextCursor is null on the last page. Items come newest first.

    • offset, total and the pagination object are gone. A request with offset is refused with 400 and a hint to use cursor.
    • limit must be between 1 and 100. A value outside that range is refused with 400 instead of quietly clamped.
    • The image categories list is no longer part of the image-library response. Ask GET /api/v1/image-library/categories for it.

    An integration that read total, pagination or categories, or stepped through pages with offset, 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.

  22. v1.85.0

    The deck list pages by cursor

    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/presentations

    The public deck list moves onto the same page contract as the rest of the API. Send limit and, for the next page, the cursor you got back; the response is { presentations, nextCursor }, and nextCursor is null on the last page. Decks come newest change first.

    • offset and the pagination object are gone. A request with offset is refused with 400 and a hint to use cursor.
    • limit must be between 1 and 100. A value outside that range is refused with 400 instead of quietly clamped.

    An integration that read pagination.total or stepped through pages with offset needs a change. The OpenAPI description has the new shape.

    MCP: list_presentations

    The MCP tool list_presentations takes a cursor and returns nextCursor to fetch the next page. It no longer returns total.

    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.

  23. v1.84.0

    Every notification, not just the latest twenty

    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 .deck file 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/notifications endpoint now pages with an opaque cursor and returns nextCursor; offset is refused with a hint to use cursor. The list filter has one form, filter=all|unread|mentions|archived: the retired unread=true and unknown filter values are refused instead of ignored. The public /api/v1 is unchanged in this release. No new dependency and no configuration change. See the 1.84.0 release on GitHub for the commit-level details.

  24. v1.83.5

    An imported PDF looks more like its source

    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.

  25. v1.83.4

    The translation view shows only fields with text

    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.

  26. v1.83.3

    Unpublished previews leave your bucket too

    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 --check to 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 new MediaProvider.withdrawPublicObject(url) method. See the 1.83.3 release on GitHub for the commit-level details.

  27. v1.83.2

    Typing a translation no longer marks it as current

    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.

  28. v1.83.1

    A duplicated deck stays a copy, and the translate view saves every edit

    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.

  29. v1.83.0

    Change what an API key may do, without a new key

    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:read or collaborators: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.

  30. v1.82.0

    A way out of the sandbox, a slimmer insert seam

    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.

  31. v1.81.0

    MCP agents can share a deck privately

    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_collaborators shows who has private access to a deck, with name and permission. add_collaborator gives one existing member of your organization view, comment, edit or admin access.

    Both follow the same rule as the editor: only someone who may manage a deck’s collaborators (the owner, the creator or an admin collaborator) 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:read for list_collaborators and collaborators:write for add_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.

  32. v1.80.0

    Deck settings as a panel, share-link log removed

    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.

    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_log table 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.

  33. v1.79.0

    Addressable settings, shorter attempt records

    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.

    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.

    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 deleted and user_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.

  34. v1.78.0

    Settings by scope, previews follow publication

    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.

  35. v1.77.5

    Reader exports without speaker notes

    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, .deck files 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.

  36. v1.77.4

    Follow-along shows the audience view

    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.

  37. v1.77.3

    Speaker notes stay with authors

    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 read permission.

    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.

  38. v1.77.2

    Safer collaboration connections

    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 Origin header 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.

  39. v1.77.1

    More reliable translation

    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 with fillMissing: true to 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 default for 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.

  40. v1.77.0

    Reproducible screenshots of new-deck dialogs

    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.

  41. v1.76.0

    Sources keep their structure

    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 doctor now reports a thumbnail cache it cannot write to, which used to surface as thumbnails that silently never appeared. pdf-parse is 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 to presentational: 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.

  42. v1.75.0

    Translate a deck side by side

    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), and slideTextRows in shared/i18n-progress.js walks a deck’s text in two languages. An AI message can carry cache: true. Details in the release on GitHub.

  43. v1.74.0

    Import a PDF as editable slides

    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.

    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: generateDeckV2 is now generateDeck, reattachAiMeta is replaced by generatedDeckToParts (both in server/utils/ai/index.js, no aliases), loadAiThemeContext moved to server/utils/themes.js, and server/utils/openai/deck.js is gone. The import progress no longer sends the phase words parse, analyze, convert and fetch. The slide-import client seam is now createDeckFilesPicker / importDeckFiles, and there is a new endpoint, POST /api/presentations/import/slides. Details in the release on GitHub.

  44. v1.73.0

    Two ways to start a deck

    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 lg breakpoint now sits at max-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 to client/views/slide-import/, and there is a new endpoint, POST /api/presentations/import/images. /api/slide-types now carries autoBackgroundPreset. Details in the release on GitHub.

  45. v1.72.0

    The editor by keyboard

    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, and npm run doctor turns red on the new theme-references check 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.

  46. v1.71.0

    The new editor flow

    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, 0 disables the cap). A browser that cannot start now answers 503 browser_unavailable instead of 500 export_failed, and npm run doctor launches the browser for real, so a Chromium that crashes on start shows up red. Forks with editor customisations should check the release on GitHub: the bulk surface and bulk-edit-modal.js are gone, bulkOnlyFields() is now fieldsOnlyFields(), and export-modal rows describe themselves with preview or file instead of open.

  47. v1.70.0

    Editor layout and imports

    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.

  48. v1.69.1

    PDF import fix

    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.org finds the collaborator ann@example.org.

    No migration and no new configuration. For the commit list, see the 1.69.1 release on GitHub.

  49. v1.69.0

    First admin and e-mail casing

    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_EMAIL and 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 need AUTH_ENABLED=false to 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.org and ann@example.org are 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 a warn line 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

    ModuleChange
    server/utils/normalize.jsNo longer exports normalizeEmail; import it from shared/email.js
    Migration 092Lowercases 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 warn in the deploy log

    New configuration: AUTH_ADMIN_EMAIL (optional). See the 1.69.0 release on GitHub for the commit list.

  50. v1.68.0

    Slide jumps and request limits

    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 #3 now 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 .deck bundle 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

    ModuleChange
    server/utils/rate-limit-redis.jscheckRateLimitRedis(key, { capacity, windowSeconds }) is removed; use allowRequestRedisDetails(key, { capacity, refillPerSec }, runWithRedis?). Keys live under ratelimit:token-bucket:; old keys expire on their own
    shared/slide-types/semantic-projection.jsThe projection option slideIds is renamed slideJumps ({ deck, document }, from slideJumpsFor())
    server/export/pptx.jsbuildPptxBuffer and buildEditablePptxBuffer take the stored deck and leave live-only slides out themselves; pass the unfiltered deck. View-only responses pin jumps with pinSlideJumps() from shared/slide-types/deck.js

    This release adds no database migration or new configuration variable. See the 1.68.0 release on GitHub for the commit list.

  51. v1.67.0

    Storage layer for forks

    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 StorageScope as 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 StorageScope as their first argument. Insert it before every previous argument; the old signatures are removed.

    ModuleChange
    server/storage/activity-events.jsdeleteOldActivityEvents(scope, olderThan): instance retention scope only
    server/storage/slide-locks.jscleanupExpiredSlideLocks(scope): instance retention scope only
    server/storage/themes.jslistSeedThemes(scope): organization or reasoned instance scope
    server/storage/settings.jsresolveSeedThemeSlug(scope, slug), installationDefaultThemeId(scope): organization or reasoned instance scope
    server/storage/tags.jsreplaceTagLinks(scope, { linkTable, rowId, tagNames }): the orgId field is removed
    server/storage/utils/index.jsgetUserIdByEmail(scope, db, email): the orgId argument is removed

    The database infrastructure leaves server/storage/, without re-exports at the old paths:

    ExportOld pathNew path
    withDbGuardserver/storage/utils/db-guard.js (also re-exported by server/storage/utils/index.js)server/db/client.js
    initializeStorage, closeStorageserver/storage/lifecycle.jsserver/db/lifecycle.js
    pendingMigrationsError, strandedFileDataErrorserver/storage/boot-check.jsserver/db/boot-check.js

    The no-op test helper __resetStorageForTests is removed, and with it uninstallFacadeStorage() in tests/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 called replaceTagLinks or getUserIdByEmail with an orgId passes 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.

  52. v1.66.0

    Security and API limits

    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=true and 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 fixed Retry-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. getRateLimitHeaders in server/storage/api-usage.js accepts only the explicit ai or exports limits; request callers use checkRequestRateLimit from server/utils/rate-limit.js. allowRequestRedis in server/utils/rate-limit-redis.js becomes allowRequestRedisDetails and returns a decision object: read its .allowed, or use allowRequest from server/utils/rate-limit.js for a boolean.

    The following 42 analytics, live-session, interaction-slide and share-link exports now take StorageScope as their first argument. Insert it before every previous argument, including the database argument in getCompletionCounts(scope, db, ...) and getValidShareLinkById(scope, db, ...). Import paths are unchanged; the old signatures are removed.

    ModuleExports receiving a new first scope argument
    server/storage/analytics/aggregations.jsgetCompletionCounts, getDetailedSlideEngagement, getInteractionHeatmapData, getPresentationAnalyticsOverview, getViewerJourneyData
    server/storage/analytics/dashboard.jsgetDashboardSummary, getDashboardTimeline, getPresentationsWithAnalytics, getSourceBreakdown, getTopPresentations
    server/storage/analytics/slide-views.jsdeleteOldSlideViews, endAllSlideViewsForSession, transitionToSlide
    server/storage/analytics/view-sessions-gdpr.jsanonymizeOldIpAddresses, deleteUserAnalyticsData, eraseAnalyticsDataForDevice, eraseAnalyticsDataForSession, exportUserAnalyticsData
    server/storage/analytics/view-sessions.jscreateViewSession, deleteOldViewSessions, getActiveViewerCount, getViewSessionsForPresentation
    server/storage/analytics/weekly-summary.jsgetTeamWeeklyAnalytics, getWeeklyAnalyticsForUser
    server/storage/interaction-slides.jsensureInteractionSlide, getInteractionSlide, updateInteractionSlide
    server/storage/live-sessions/close.jscloseSession
    server/storage/live-sessions/db.jscountPersistedSessions, deletePersistedSession, hydrateSession, hydrateSessionsForPresentation, persistSession, schedulePersist, sweepExpiredSessions
    server/storage/live-sessions/sse.jstouchSessionSync
    server/storage/share-links/access-log.jsgetShareLinkAccessLog, logShareLinkAccess
    server/storage/share-links/crud.jsgetValidShareLinkById, shareLinkCreatorNames
    server/storage/share-links/guests.jsrequestGuestVerification
    server/storage/share-links/index.jscleanupExpiredShareLinks

    Pass 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.

  53. v1.65.0

    Editing and fork maintenance

    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 StorageScope as their first argument. Insert it before every previous argument, including the database argument in ensureMembership(scope, db, ...). Import paths are unchanged; the old signatures are removed.

    ModuleExports receiving a new first scope argument
    server/storage/collaborators.jsaddCollaborator, getCollaboratorPermission, listCollaborators, removeCollaborator, updateCollaboratorPermission
    server/storage/display-identity.jsresolveDisplayNames, resolveNamesForAddresses
    server/storage/identity-resolver.jsresolveIdentity, resolveIdentityByEmail
    server/storage/identity-verification.jsverifyIdentityConsistency
    server/storage/identity.jsgetUserByEmailGlobal, resolveActiveMembership, resolveActiveOrganization
    server/storage/login-membership.jsensureMembership
    server/storage/password-reset.jscleanupOldAuditLogs, getPasswordChangedAt, hasDatabaseCredentials, logAuthEvent, verifyUserPassword
    server/storage/user-organizations/memberships.jsaddMember, countOrganizationMembers, getMembership, getMembershipByEmail, getOrganizationMember, listOrganizationMembers, listUserOrganizations, removeMember, transferOwnership, updateMemberDesigner, updateMemberRole
    server/storage/user-organizations/organizations.jscreateOrganization, deleteOrganization, getOrganizationById, getOrganizationBySlug, updateOrganization

    Use 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.

  54. v1.64.0

    Fork maintenance

    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 pathNew path
    shell/20-editor-layout.csseditor/20-editor-layout.css
    shell/21-editor-skeleton.csseditor/21-editor-skeleton.css
    shell/107-collab-presence.csseditor/107-collab-presence.css
    shell/05-avatar.cssuser/05-avatar.css
    shell/99-notification-bell.cssuser/99-notification-bell.css
    shell/96-activity-feed.csslist/96-activity-feed.css
    list/17-deck-grid.csseditor/17-deck-grid.css
    settings/103-data-source.csseditor/103-data-source.css
    editor/modals/10-modals-base.csscomponents/10-modals-base.css
    editor/modals/11-modals-loading.csscomponents/11-modals-loading.css
    editor/106-shortcuts-overlay.csscomponents/106-shortcuts-overlay.css
    components.csscomponents/00-components.css
    editor/modals/13-modals-misc.csstheme-select/13-theme-picker.css
    shell/110-sandbox-banner.csslist/110-sandbox-examples.css
    share-viewer/75-share-viewer.csseditor/modals/19-modals-revoke.css

    Only the theme-picker section moves out of 13-modals-misc.css, the example shelf out of 110-sandbox-banner.css, and the revocation dialog out of 75-share-viewer.css. Their other styles remain with the original feature. Slide-picker and translation variants from the modal base live in editor/modals/10-modals-editor.css. components.css is 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.

  55. v1.63.0

    Fork maintenance

    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, both enforceSlideLocks and enforceSlideWritePolicy now take (scope, opts) instead of ({ctx, ...opts}). Pass the organization scope as the first argument and remove opts.ctx. Their import path stays the same.

    Theme seed helper for forks

    upsertThemeSeeds(db, seeds) moved from server/utils/theme-seeds.js to server/storage/themes.js as upsertThemeSeeds(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.

  56. v1.62.0

    Security release

    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 in organizations.settings.translationInstructions: terminology applies to every target language, while styleByLanguage[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 pathNew path
    client/views/presenter/step.jsclient/lib/slide-runtime/step.js
    client/views/presenter/auto-advance.jsclient/lib/slide-runtime/auto-advance.js

    For the commit list, see the 1.62.0 release on GitHub.

  57. v1.61.0

    Maintenance release

    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.js with the public entry point client/views/tags/index.js for createTagEditor. 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.

  58. v1.60.0

    Maintenance release

    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/ to client/views/comments/, with no compatibility shims:

    Previous pathNew path
    client/views/editor/comment-markers.jsclient/views/comments/comment-markers.js
    client/views/editor/comments-api.jsclient/views/comments/comments-api.js
    client/views/editor/comments-panel.jsclient/views/comments/comments-panel.js
    client/views/editor/comments-panel-actions.jsclient/views/comments/comments-panel-actions.js
    client/views/editor/comments-panel-renderers.jsclient/views/comments/comments-panel-renderers.js
    client/views/editor/comments-panel-sse.jsclient/views/comments/comments-panel-sse.js
    client/views/editor/comments-read-state.jsclient/views/comments/comments-read-state.js

    Import createCommentMarkers, createCommentsApi and createCommentsPanel through the public client/views/comments/index.js entry 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.

  59. v1.59.1

    Maintenance release

    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.

  60. v1.59.0

    Feature release

    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 pathNew path
    client/lib/util/clipboard.jsclient/lib/dom/clipboard.js
    client/lib/format/lang-selector.jsclient/lib/dom/lang-selector.js
    client/lib/util/qr.jsclient/lib/dom/qr.js
    client/views/ui-mode-switcher.jsclient/lib/dom/ui-mode-switcher.js
    client/lib/util/file.jsclient/lib/net/file.js
    client/lib/util/debug.jsclient/lib/state/debug.js
    client/lib/util/dev-runtime.jsclient/lib/state/dev-runtime.js
    client/lib/util/id.jsclient/lib/state/id.js
    client/lib/collab/live-doc-binder.jsclient/views/editor/live-edits/live-doc-binder.js
    client/lib/collab/presence-session.jsclient/views/editor/live-edits/presence-session.js
    client/lib/collab/textarea-merge.jsclient/views/editor/live-edits/textarea-merge.js
    client/views/notes-join.jsclient/views/notes/notes-join.js
    client/views/present-window.jsclient/views/presenter/present-window.js
    client/views/theme-select.jsclient/views/theme-select/index.js

    The 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.

  61. v1.58.0

    Feature release

    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 raw value, 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 error and message. JSON-RPC errors keep their protocol shape.

    Breaking changes for forks

    • QR helpers move from client/lib/slide-runtime/poll.js to client/lib/util/qr.js. Update imports of renderQrToCanvas and createQrDataUrl. The unused getOrCreateVoterId and mountPollBars exports are removed.
    • server/utils/slug.js and its safeSlug export are removed. Use generateSlug from shared/slug.js; pass { fallback: 'presentation' } where that default is needed. Explicit slugs are checked with isValidSlug, 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: true can 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_by and updated_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 with NODE_ENV=development; production logging is off. Set it explicitly to enable or disable it. Fork callers must stop passing the removed per-call enableLogging option.
    • 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.

  62. v1.57.0

    Feature release

    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:B10 in 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_ID on 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_URL and OG_DESCRIPTION set, 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 gets setOptions(patch).

    For self-hosters and forks

    No database migrations. Three new variables, all optional: OG_IMAGE_URL and OG_DESCRIPTION for link previews, BUNNY_LIBRARY_ID for 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.deck folds to deckyard.deck; image-slide layout folds into fit and bleed; altNl and altEn fold into alt; the legacy bgImage, bgAlt and bgImagekitFileId on 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 through instanceKeys.
    • MCP tightens up. add_slide and update_slide store content as sent or refuse it with 400 and errors; they no longer repair it silently (validation: 'fix' stays). The id alias is gone: every deck-scoped tool takes presentationId. get_slide_types returns the type’s real sample as example. A deck without lang defaults to nl on convert, iterate and append.
    • API: a refused action on a promoted question answers closed (409); locked now means only the slide lock (423). GET /api/jobs/:id no longer returns error for a failed job, only message when there is a sentence for the caller. New GET /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 require slideTypes and throw without it; loadSlideTypeContext is replaced by loadSlideTypeOffer(); a custom type’s ai.examples entries take the form { variation, content }; the title-view seam in core-layouts.js exports titleInlineEdit and renders without a ground when view.background is null. Removed: validate-slide-structure.js, shared/phosphor-icons.js, shared/slide-schemas.js, TitleView.bgImage, resolveSlideBgImage, BUNNY_PLAYER_COLORS and the ?s= alias for ?slideId=.
    • A global box-sizing: border-box reset 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-status with .is-failure.

    For the complete, commit-level list, see the 1.57.0 release on GitHub.

  63. v1.56.0

    Feature release

    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=strip to 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 /go speak the deck’s language, then the browser’s, then English. A saved preference or ?locale= still wins, and /go is 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 Error with a message meant for the user should throw an AppError with 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_LOCALE lives in shared/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>.one and <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 updatePresentationRow does 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.

  64. v1.55.0

    Maintenance release

    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 analytics sort or period, a non-boolean keepAsCollaborator or isViewOnly, and a missing or unknown translation language are all a 400 invalid with the field in details.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_presentations and list_recent_comments know collection (your decks plus the organisation-visible ones, as Home shows them). all now includes organisation-visible decks, and owned matches 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 502 bad_gateway that 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_URL instead 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_slide refuses 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.

  65. v1.54.0

    Feature release

    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-slide type 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_EMAILS does 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 lose textSwatches. 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 post quotes[]; the flat keys are refused by strict validation. Stored decks fold on read.
    • A fork type’s CSS lives only in custom/styles/. A custom/slide-types/*.js type 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 through shared/slide-types/core-layouts.js instead of copying it.
    • The ImageKit slot passes createImageKitPanel and returns { el, detach }; the picker owns the dialog. Passing openImageKit throws.
    • 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 (header X-Image-Slides lists the image slides), and MCP export_presentation offers pptx-editable with 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) and PUBLIC_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_EMAILS can 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.

  66. v1.53.0

    Feature release

    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_ONLY or 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_ENABLED is 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.

  67. v1.52.0

    Maintenance release

    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_FEATURE is now NOTION_ENABLED and switches the whole /api/notion/* module (off by default). An installation that only set NOTION_SECRET must now also set NOTION_ENABLED=true.
    • DISABLE_ANALYTICS is now EXTERNAL_ANALYTICS_ENABLED, with the meaning flipped: true means 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_ENABLED off, /api/data-sources/* answers 404 (it was 403).
    • With RSS_FEED_ENABLED off, /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_SECRET and DISABLE_ANALYTICS and 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.

  68. v1.51.0

    Feature release

    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/comments answers a slideId that names no slide of the deck with 404 slide_not_found (it was 400), next to the existing parent_not_found.
    • MCP add_comment and reply_to_comment now 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 lang is refused before any work is done.
    • For fork code: maybeFireWebhook(repoRoot, origin, event) takes an origin string where it took the request, and notifyCommentCreated and notifyMentionsAdded no longer take a request. notifyCommentCreatedInApp and routes/api/presentations/comments-shared.js are gone; import from server/services/comments.js instead.

    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 doctor warns when an https instance runs without TRUST_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_NAME in 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 runs npm 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.

  69. v1.50.0

    Feature release

    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:

    OldNew
    PDF_EXPORT_TIMEOUT_MSEXPORT_RENDER_TIMEOUT_MS
    PDF_EXPORT_IMAGE_COMPRESSIONEXPORT_IMAGE_COMPRESSION
    PDF_EXPORT_IMAGE_MAX_PXEXPORT_IMAGE_MAX_PX
    PDF_EXPORT_IMAGE_QUALITYEXPORT_IMAGE_QUALITY
    PDF_EXPORT_IMAGE_RETINA_SCALEEXPORT_IMAGE_RETINA_SCALE

    There 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: pdfImageEmbedTransform is now exportImageEmbedTransform, and pdfImageCompressionConfig is now exportImageCompressionConfig.

    Running in production

    • npm run doctor checks 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/*.css only 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 install if 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.

  70. v1.49.0

    Feature release

    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_CLAIM tells Deckyard which claim holds a user’s groups.
    • Extra scopes. OIDC_EXTRA_SCOPES adds scopes on top of openid 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:seams lists them, and the repository’s CLAUDE.md is stable, so a fork can replace it with its own.
    • Images in fork styles reach the export. Local image url()s in custom/styles/*.css are 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.js is now client/lib/dom/index.js, and view entries live at client/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.

  71. v1.48.0

    Feature release

    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 to true, so nothing changes unless you set it. Set it to false and a regular user gets a 403 on “new organization”; only instance admins can create one. It only applies with MULTI_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 example eu.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 operationId in /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.

  72. v1.47.0

    Feature release

    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 drops font_variants.url) runs automatically on startup. If you had already uploaded fonts, run node scripts/privatize-font-variants.js --apply once afterwards to move them to private storage.

    Run it with --check first 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.

  73. v1.46.0

    Feature release

    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.x while 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-card and .sfi-card-title become .sfi-qr-plate, .sfi-link and .sfi-method-label, and the .sfi-qr-wrap wrapper 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) and 087 (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.

  74. v1.45.0

    Feature release

    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, and default is 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 contentByLang that 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.

  75. v1.44.0

    Feature release

    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 createdAt and updatedAt values. A slide-type change converts the slide or is refused; MCP then points the agent to the right route. Invalid-slide errors return details as an object.
    • Language-version keys must be canonical. Deck and library writes reject aliases such as en where the canonical key is en-GB.

    For self-hosters and forks: database migrations 083_canonical_version_keys.js and 084_library_canonical_version_keys.js convert 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.

  76. v1.43.0

    Feature release

    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.

  77. v1.42.0

    Feature release

    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: themeId and language now return a 400 error; use theme and lang instead.

    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 essential beside required in MCP get_slide_types and GET /api/v1/slide-types/:type/schema.

    Fixes and API clients

    • The public API uses theme and lang consistently. Requests using themeId or language now receive a 400 response naming the correct field. Responses report the actual theme and language, and a PUT with theme changes 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.

  78. v1.41.1

    Maintenance release

    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.

  79. v1.41.0

    Feature release

    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.

  80. v1.40.0

    Security release

    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 example Sign 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_URL puts 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.

  81. v1.39.0

    Feature release

    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_TAGS to 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.

  82. v1.38.2

    Maintenance release

    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.
    • Searching tags finds what you type. A typed % or _ is a character, not a wildcard, and names such as İstanbul are 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.

  83. v1.38.1

    Maintenance release

    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.

  84. v1.38.0

    Feature release

    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:migrate is part of installing. The one-line installer runs it before it starts anything, and names the PostgreSQL requirement before it spends minutes on npm install. The Docker path brings its own database and is unchanged. See the quickstart.

    Fixes

    • AI_ENABLED=false now 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:migrate after 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.

  85. v1.37.0

    Feature release

    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.

  86. v1.36.1

    Security release

    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 in If-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.

  87. v1.36.0

    Feature release

    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 set TRASH_RETENTION_DAYS higher 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=7 says 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_trashed instead 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 .deck bundle gives a plain reason (“the file is not a zip archive”) instead of a library error, and a bundle with a broken manifest.json or deck.json names 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.

  88. v1.35.0

    Feature release

    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 .deck bundle now carry every language of a deck, so ?lang= on the JSON export no longer selects one; content is the dominant language. /api/v1/schema/deck.json describes that export instead of the stored model: format, version and title are required, id, schemaVersion, created, modified, settings and description are gone, and slides carry no id. An import refuses a deck whose language, or one of its translation tags, the instance does not support, or whose lang contradicts 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 a tel: link, and a LinkedIn link without https://, 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. Second used 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 with 1. 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 .deck from the app. The export menu has a .deck row 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 .deck exports 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 .deck exports.

    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 a mailto:.
    • 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_alt with details). It used to be a “publish anyway” confirm for two slide types.
    • 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 old print-slide and 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 with data-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 --help lists 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: true on a code field, nameKey on an image field, unitKey, hrefKey, headingKey, duration: { secondsKey }, scale, the url and email field types, orderedWhen, rowHeader: 'first', role: 'aside' with kindKey, role: 'attribution', termWhen, defaultFromOption with copyKey, a datasetSummary function, and semantic: true on an enum. See docs/developer/slide-types.md and docs/reference/reflowable-html-export.md in core.
    • The .deck bundle is bundleVersion 3 (docs/reference/deck-bundle.md); a bundle with an unknown version is refused rather than half-read. Import responses say bundledTheme and bundledSlideTypes when 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.

  89. v1.34.0

    Feature release

    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.

  90. v1.33.5

    Maintenance release

    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.

  91. v1.33.4

    Maintenance release

    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-rendered DOM event is gone. To wait for a server-rendered slide, use slideRendered(el) from client/lib/slide-runtime/slide-render.js, which resolves true once the markup is in and false if 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.

  92. v1.33.3

    Maintenance release

    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.

  93. v1.33.2

    Maintenance release

    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.

  94. v1.33.1

    Maintenance release

    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=min is 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.

  95. v1.33.0

    Feature release

    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-slide is one image. Decks that stored several images on it are upgraded when they are read, with no migration to run. An API client that wrote images[], imageFit or textColumns to that type should write an image-set-slide instead.

    PowerPoint export

    • Speaker notes travel with the PPTX, as PowerPoint notes on each slide. They used to arrive only as a separate notes.md or notes.docx in the handoff zip. The document is also authored under your app name now.
    • A new export row, “PPTX template”, downloads an empty .pptx with 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_DIR is 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_slides refuses a key the type does not declare (unknown_field) instead of dropping it, and accepts every example from get_slide_types. update_slide with a type refuses 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. See docs/reference/slide-type-fidelity.md in core.
    • New optional field declarations: itemLabelField on an items field, mediaRef on a string field, foldUnofferedTo on 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.

  96. v1.32.0

    Maintenance release

    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 under server/storage/ has taken a storage scope since 1.4.0 and refuses a bare repoRoot string, but the only builder for that scope lived inside core and never crossed the registerCustom seam. A fork tool that wanted to reach storage could therefore pass ctx.repoRoot and fail for every keyed caller, or copy core’s logic into the fork and re-copy it on the next merge. storageScopeOf now sits on the seam’s ctx, next to repoRoot rather than in its place: repoRoot is still the disk path for themes and uploads, it is just not a storage argument. Core’s docs/reference/mcp-server.md shows 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.

  97. v1.31.0

    Feature release

    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 migrate after 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

    • logoOnDark and logoOnLight next to assets.logo, plus titleLogoOn* 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.details has a fixed shape per error code. An API consumer can switch on code and know which fields details carries.
    • 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 readOnly flag instead of dropping them. An explicit permission: null on a gated tool now throws at start-up rather than silently opening it. This only matters with a custom/mcp-tools.js of 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.

  98. v1.30.0

    Feature release

    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 in docs/reference/feedback-surfaces.md are now the one way to show a refusal, and a guard test rejects a hand-rolled *-error class. 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.

  99. v1.29.0

    Feature release

    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, ui and a configured allowedOrigins list 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|EN pair 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-universal becomes .slide-title and .slide-lijstje becomes .slide-list on the title and list types, and .sfi-card-kicker becomes .sfi-card-title on 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. The tsu-* and .lijst* families are unchanged. Deckyard stays on 1.x while 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 scaffold only 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 details with field, index, itemIndex and reason, 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.

  100. v1.28.0

    Feature release

    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 in custom/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|both and 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.

  101. v1.27.2

    Maintenance release

    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 enabledThemes allowlist is enforced on the server, in every picker. GET /api/themes used 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_THEMES environment variable, beside DEFAULT_THEME and 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.

  102. v1.27.1

    Security release

    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.

  103. v1.27.0

    Security release

    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=abc no 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 a null limit 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.x while 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:validate is gone. Every check it made is now a test.
    • The i18n scripts share one flag vocabulary. --dry-run and --report and the bare language argument are replaced by --apply and --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.

  104. v1.26.0

    Feature release

    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 options array. They were option1 through option4 on a poll and option1 through option10 on 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, write options: [{ 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 images field 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.

  105. v1.25.0

    Feature release

    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”.

    • 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-scale works. 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.

  106. v1.24.0

    Security release

    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-capture and .lead-capture-* CSS classes are no longer emitted. If you style slide types in a fork, those names are gone.
    • The /api/leads endpoints are removed, along with the lead storage, the GDPR token store and the owner-notification e-mail with its two templates.
    • The lead.submitted webhook is gone, and its admin row with it. Seven configurable events remain.
    • The leads.retentionDays setting, the lead e-mail preference and the admin “Lead capture” card are removed, as are /my-data and the analytics leads tab.
    • A migration drops the lead_submissions and gdpr_verification_tokens tables. 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/leads was 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 into error: "invalid" plus details.field, which names the input the code used to encode in its suffix. The status is unchanged: 400 throughout. Gone are invalid_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_usage and invalid_weight.

    The four 401 codes stay: invalid_password, invalid_token, invalid_or_expired and invalid_or_revoked say 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.

  107. v1.23.0

    Security release

    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-src and style-src need 'unsafe-inline'. What it refuses is a script from a host nobody decided on, and default-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=scaleway becomes =s3
    • SCW_ACCESS_KEY / SCW_SECRET_KEY / SCW_BUCKET / SCW_REGION / SCW_ENDPOINT / SCW_CDN_URL become S3_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_ENDPOINT is mandatory under the new names: MEDIA_STORAGE_MODE=s3 refuses to boot without it. An untouched legacy install is the one exception, where an unset endpoint is still derived as https://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-library returns createdById instead of createdBy. 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, the presentation_locks and lock_requests tables and the USE_DB_LOCKS variable. 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-ownership answers 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_found is 404, own_question is 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.

  108. v1.22.0

    Maintenance release

    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 deckyard is now amethyst, 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 in custom/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-v2 and /api/ai/wizard-v2/outline are removed; the streaming endpoint /api/ai/wizard-v2/stream is 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.

  109. v1.21.0

    Feature release

    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.signingSecret and every delivery carries x-sb-signature: sha256=<hmac-of-body> next to the existing x-sb-event header. Absent when the secret is unset, so nothing changes until you opt in. There is an admin field for it.
    • actor.id carries the stable user id, not the address. The address stays available as actor.email, a contact value rather than an identity. If your receiver keyed on actor.id as an address, it needs updating.
    • The event slide.added_to_team_library is renamed slide.added_to_organization_library, in the payload and in the x-sb-event header. There is no alias, so a receiver matching the old string stops recognising it silently.
    • The user-agent is Deckyard-Webhook/1, replacing presentation-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.retentionDays is 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, not scope. It applies to list_presentations and list_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 is slide-library/organization.json where it was team.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.

  110. v1.20.0

    Feature release

    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_AI becomes AI_ENABLED
    • DISABLE_UPLOADS becomes UPLOADS_ENABLED
    • DISABLE_IMAGE_LIBRARY becomes IMAGE_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.md records 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:refreshed broadcast 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.

  111. v1.19.0

    Feature release

    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-color and --t-chapter-text-color become --t-slide-bg-dark-text; --t-quote-author-color is renamed --t-color-accent-on-dark.
    • --t-list-item-title-letter-spacing is removed.
    • The legacy aliases are no longer emitted. --t-primary, --t-accent, --t-bg-dark and --t-brand-1/-2 are gone; brand slots --t-color-brand-{1..3} fill from brandColors.

    Fixed

    • DELETE /api/leads/my-data reaches 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 sendEmail is 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-size transitive 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.

  112. v1.18.0

    Feature release

    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, scope becomes visibility. The values are private and organization; workspace is gone as a value. The internal PATCH /api/presentations/:id/scope becomes /:id/visibility, and the webhook event presentation.moved_to_workspace becomes presentation.moved_to_organization, carrying fromVisibility and toVisibility. “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.

  113. v1.17.0

    Feature release

    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 ownerId as 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.js confirms 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.enabled is 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 turn analytics.enabled off, 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_URL wins over the individual DATABASE_* variables in db: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.

  114. v1.16.0

    Feature release

    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.

  115. v1.15.0

    Feature release

    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_MODE now has one spelling per backend. postgresql used to be a silent alias for postgres, 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.example documents the database block as ordinary configuration rather than as an opt-in.
    • The installer fails fast on a stale DATABASE_HOST instead 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=file explicitly.

    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.

  116. v1.14.0

    Security release

    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.md documents 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.

  117. v1.13.0

    Security release

    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 in package-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-face came back first, which was cyrillic-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 latin plus latin-ext, with a real unicode-range on 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.json records the exact URL, SHA-256 and byte size of every file, and the installer refuses anything that does not match. Until now the contents of assets/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.

  118. v1.12.1

    Maintenance release

    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-slide and card-stack-slide are 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-grid as the rebuild target; it takes the same items[] shape, and scripts/migrate-slides.js will 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.js was 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.

    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.

  119. v1.12.0

    Feature release

    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.

  120. v1.11.0

    Feature release

    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 type is 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[].type off 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 slideTypes manifest 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[].type is a 400 on 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 structure facet is normative, type ids are reverse-DNS with -slide dropped, and the catalogue now carries three tiers plus a fallback facet 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.

  121. v1.10.0

    Feature release

    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 runs node scripts/migrate-lijstje-slide.js once. 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-slide is retired. It was a second name for list-slide and 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.js once, after upgrading.
    • Anything that slips through both renders as an archived slide pointing at list-slide, not as a broken slide.

    split-partner-title-slide also 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=min still 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 default move. The violet deckyard theme 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.

  122. v1.9.0

    Feature release

    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 soft table 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; on plain and panel it 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.

  123. v1.8.0

    Feature release

    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.

  124. v1.7.1

    Maintenance release

    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.

  125. v1.7.0

    Feature release

    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-slide was offered a field that does not exist. Agents were told to fill videoUrl; the type reads source. So every AI-written video slide fell back, silently, to the demo video.
    • payoff-slide was offered a tagline that 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.deck

    The format field inside a portable deck, and the media type of a .deck package, used to say slidecreator - a placeholder from December 2025, written before this product had its name. They now say deckyard.deck and application/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 the format field, 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.

  126. v1.6.0

    Feature release

    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=min for embedded decks

    Add ?ui=min to 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 plain aspect-ratio: 16 / 9 instead 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 F for fullscreen all keep working.
    • Without the parameter nothing changes, so existing links are untouched.

    The name and meaning match the ui option 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, autoplay and interval existed but were written down nowhere.

    For the complete, commit-level list, see the 1.6.0 release on GitHub.

  127. v1.5.1

    Maintenance release

    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, and puppeteer-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.errors and 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-core 25. 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 feed 5 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.

  128. v1.5.0

    Feature release

    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 #ea580c at 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 midnight is 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 midnight and 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.

  129. v1.4.0

    Feature release

    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_types tool 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_types response, 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-slide type 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 \d collapses to a literal d; it never matched, and had been broken for as long as the export runtime has existed.
    • GET /api/v1/image-library answered 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/tags and POST /api/tags read 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 message instead of error, and no longer include an ok field. 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-slide removal above is that change.

    For the complete, commit-level list, see the 1.4.0 and 1.4.1 releases on GitHub.

  130. v1.3.0

    Feature release

    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_ENABLED was 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 conflict and locked codes and the revision details preserved. Status codes are unchanged and the app is unaffected; if you built against the API directly, the error field now carries a machine code and the human sentence moved to message.

    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.

  131. v1.2.0

    Maintenance release

    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_PROXY and TRUSTED_PROXY_COUNT make 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.

  132. v1.1.0

    Feature release

    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 .deck bundles 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 .env setup, 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.

  133. v1.0.0

    Public launch

    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.