Skip to content

Leafdesk 0.5.1

Choose a tag to compare

@erhanerbas erhanerbas released this 12 Oct 08:06

Upgrading from 0.5.0

  • Migrations (0049–0059) run automatically when the container starts.

  • Docker Compose: download the new docker-compose.yml. It needs POSTGRES_PASSWORD for the
    bundled database (see below) and passes AUTOMATION_DATE_CHECK_SECONDS to the app.

  • Automations that set a date to today use the day in the time zone of whoever saved them
    last. Automations saved before this release count as saved in UTC until someone saves them
    again; before, they used the server's day.

  • TRUSTED_PROXIES behind a proxy with a public address. Unset, the app now trusts only a
    proxy that connects from a private address. If yours connects from a public one (a CDN in front
    of the app, a proxy on another server), set TRUSTED_PROXIES=1 (or 2 for a CDN plus a proxy),
    or every visitor counts as the proxy's address. Proxies on the same machine or Docker network
    need nothing.

  • POSTGRES_PASSWORD is required for the bundled database. Download the new
    docker-compose.yml: with COMPOSE_PROFILES=bundled-db and no EXTERNAL_DATABASE_URL, it
    refuses to start until .env sets POSTGRES_PASSWORD. If you never set one, your database
    has the old default: add POSTGRES_PASSWORD=leafdesk (and change it inside PostgreSQL later if
    you like). An external database needs nothing.

  • Allowed email domains must be verified. A workspace's allowed domains stop letting people
    join, ask to join or find the workspace until an owner proves each one: Settings → Security
    shows the DNS TXT record to add at _leafdesk-sso.<domain> and a Verify button. Domains its
    single sign-on already verified keep working. Tell your workspaces' owners before upgrading. The
    record's value is derived from BETTER_AUTH_SECRET; changing the secret changes it for domains
    not verified yet.

  • Connection events start untrusted. Migration 0053 adds a "Trust what this service sends"
    switch to each event trigger, off for existing ones: an agent run started by an event now waits
    for an owner's approval at its first connection tool call. In Settings → Connections → Events,
    trust only the triggers of services where nobody outside the workspace can make it send events.

  • Pages more than 64 levels deep. Migration 0055 gives no access to a page the access rule
    can't follow to the top level within 64 steps, and sharing changes above such a page may be
    refused while it has entries of its own. Find them before upgrading, and move them or a page
    above them higher:

    WITH RECURSIVE d AS (
      SELECT id, 0 AS depth FROM page WHERE parent_id IS NULL
      UNION ALL
      SELECT p.id, d.depth + 1 FROM page p JOIN d ON p.parent_id = d.id WHERE d.depth < 200
    )
    SELECT id, depth FROM d WHERE depth > 64;
  • Migrations delete stored data nobody used. Migration 0055 clears the access, refresh and ID
    tokens sign-in providers handed over (Leafdesk never read them). Migration 0056 deletes the page
    and property entries, calendar feeds and workspace-only API tokens of people who are no longer
    members of that workspace; none of them worked while they were out, but they came back if the
    person was let in again.

  • Verification links on another device. Someone who signs up and opens the verification email
    in a browser that isn't signed in to the account is signed out everywhere else and asked to
    choose a new password, through a link emailed to the address, at their next password sign-in.

Added

  • Rows open beside the view. Clicking a row or card in a table, board, calendar, gallery, list
    or timeline, also in a database inside a page, opens it in a panel at the right, and the view
    stays usable next to it: title, icon, properties (with "N more properties"), the body edited
    together with who's there, comments (their panel opens alongside) and the row's menu, where
    Duplicate opens the copy in the panel and Move to trash closes it. The panel steps to the
    previous and next row in the view's order (buttons, or K and J outside text, listed under
    Keyboard shortcuts), opens the row as a full page or copies a link to it, and closes with its
    button or Esc, giving focus back to the row. The open row is in the address (?row=): a link or
    a reload opens it again, Back closes it, and moving between rows doesn't add to the history. Its
    width can be dragged (400–1,200 pixels, leaving the view at least 320) and is kept in the
    browser. A row trashed or moved out of its database by someone else, or one the viewer can't
    see, shows that in the panel. A view's layout menu has "Open rows in: Side panel / Full page"
    (side panel unless set), and MCP views take open_rows (panel or page); get_database
    shows open_rows: "page" on views that open full pages. ⌘/Ctrl-click and middle clicks open
    the row in a new tab; phones and offline pages open the full page. Published pages are
    unchanged. No migration.
  • Automations when a date passes, and moving rows to the trash. A new trigger, A date
    passes
    (date_passed), starts an automation for each row whose date is at least a number of
    days past (0 for the day itself): a date property's start, when the row was added, or when it was
    last edited, optionally only for rows whose status is in some groups (to do, in progress, done; a
    row without a status counts as to do). So "rows not edited for 30 days that aren't done → set
    Status to Stale", "rows whose Completed on is 14 days past → move to the trash" and "the day a
    Due date comes, for rows not done → notify the assignee" are each one automation. Dates without
    a time count by the calendar of the time zone of whoever saved the automation last (the
    browser's in the app, time_zone over MCP, UTC without one), and the summary names that zone
    when it isn't the viewer's; times and added/edited times count by the clock. "Today" set by any
    automation is that day in the same time zone (it was the server's), and "whoever made the
    change" isn't offered for a date passing, which nobody's change starts. The server
    looks every 10 minutes (AUTOMATION_DATE_CHECK_SECONDS, one server at a time) and runs each row
    once per value of its date, again only once that date changes and passes again; a row restored
    from the trash doesn't run again for the same date, and the automation's own edit doesn't count
    as one. Rows that already match run at the next check, at most 100 per automation per check, and
    the dialog says how many match before saving. Rows in the trash, templates and databases in the
    trash are left out; a deleted date or status property, or a person who lost full access, pauses
    the automation with the reason shown in its settings. A new action, Move to trash
    (move_to_trash), always last and not with Run an agent, works with every trigger; the rows it
    trashes are restorable as usual, go with the trash retention, and are recorded in the audit log
    as page.trashed by the automation (a new actor kind, shown as "person (automation “name”)").
    Runs show in the run history as "Date passed" with the date; webhooks get the event
    row.date_passed with that date; notifications say the row's date has come. MCP's
    create_automation and update_automation take both and a time_zone, list_automations
    describes them and shows each automation's time_zone and a paused one's check_error,
    list_automation_runs gives each run's date.
    Migration 0058 adds the automation_fire table, automation_run.basis,
    database_automation.time_zone and .check_error, and the audit actor kind. Covered by
    scripts/timed-automation-e2e.ts and unit tests.
  • Row activity. A row page has an Activity section under its properties, closed until opened
    (each browser remembers the choice): who created the row and who changed its title or a value,
    from what to what, newest first, a page at a time, updating while others edit. Every way values
    are written counts (the app, bulk edits, forms, quick add, MCP, the REST API, automations and
    agents, which are marked as such); a new row, also from an import or a template, is one
    "created" entry. Changes by the same person to the same value within five minutes are one entry
    (typing in a text, number or the title), and one that ends where it started leaves none.
    Formulas, rollups and who/when values aren't recorded. Viewers see the entries about values they
    may see in the row; linked rows they can't open read as "No access", and entries about a
    deleted property are left out until it is restored. Text is kept with credentials masked. MCP
    clients read it with list_row_activity, the REST API at GET /api/v1/rows/{id}/activity. The
    activity is kept apart from the audit log, which still leaves row content out, and goes with
    the row when it is deleted for good. Migration 0059.
  • Deleting a workspace. Owners find Settings → General → Deletion. The dialog asks for the
    workspace's name, typed as shown, and the same proof as deleting the account (the password, a
    two-step code, or a recent sign-in); a mistyped name is refused before the proof is checked, so
    it doesn't use up a code. The workspace goes for every member, with its pages, databases,
    comments, teamspaces, groups, agents, connections and audit log; open editors of its pages are
    disconnected, everyone's sign-ins through its single sign-on connection end, and its files are
    removed from storage in the background. Everyone else in it gets an email saying who deleted it,
    in their language (when email is set up); the audit log goes too, so the server log records it.
    Someone left without any workspace gets a new personal one, as after leaving the last one.
  • Health endpoints. GET /api/health answers 200 {"status":"ok"} whenever the server
    handles requests, without reading a session or the database. GET /api/health/ready also checks
    that the database answers within 2 seconds and has the newest migration this version ships with
    (a newer one, after going back to an older image, still counts as ready): 200 {"status":"ok"},
    else 503 with which check failed, in fixed words only (unreachable, timeout, pending,
    unknown), never the database's error text; the server log gets the error code once per change.
    Both are open without sign-in, never cached and set no cookie. docker-compose.yml marks the app
    healthy on /api/health/ready; the image's HEALTHCHECK uses /api/health, so a database
    outage doesn't get the container replaced. No migration.
  • Calendar subscription of assigned rows. The home page's "Assigned to you" header has a
    sync button that gives each person a secret address of their own per workspace, for their
    calendar app: the rows that list would show, from every database, that have a date they may see,
    as events at their times, else all day, ranges as ranges, titled by the row with its database's
    name and a link to it. Rows that are done, in the trash, templates, rows they can't open and
    dates or statuses hidden from them by property access are left out or decide nothing, as on the
    home page; it is not cut at the home page's 25 but holds what the 1,000 rows assigned to them
    that were edited last leave once done ones are out. The calendar is named after the workspace
    and "Assigned to you" in the language they last used. Shown once; it can be replaced or turned
    off, and it stops while the workspace's export or connected apps are off or once they leave the
    workspace. The section now stays, saying nothing is assigned, when the list is empty, so the
    address can be made beforehand. Migration 0052.
  • Import Word documents. Import has a Word tab that takes one or more .docx files, each
    becoming a page under the chosen page or teamspace: titled by the heading it starts with (or its
    Title paragraph), else by its file name, with headings, bold, italic, underline, strikethrough,
    links, nested lists, tables with their header row, quotes, monospace paragraphs as code and
    footnotes as a list at the end. Pictures in the document are uploaded to the page; ones linked
    from outside it, of a type pages can't show or over the upload limit are left out and listed.
    Documents are read in a worker with time and memory limits; a damaged or old .doc file is
    refused with nothing created. Recorded in the audit log like other imports. No migration.
  • Restore deleted properties and views. Deleting a database property or view no longer erases
    it: "Deleted properties" (at the bottom of a view's Properties menu) and "Deleted views" (under
    the add-view button) list what was deleted, by whom and when, and the day it goes for good, with
    Restore and Delete permanently. People who can change the database's properties see them; a locked
    database refuses both. A restored property comes back with its row values and with the filters,
    sorts, grouping, form questions and other view settings that named it; formulas and rollups that
    showed it as missing work again. A name taken meanwhile gets a number ("Estimate 2"). Deleting
    either side of a two-way relation now deletes the pair (it used to make the other side one-way),
    and restoring either brings both back, unless the other side is in a database you can't edit or
    that is locked: then only your side goes and the other becomes one-way. While deleted, a property
    is left out of views, row pages, filters, CSV and Markdown exports, the workspace backup, search,
    the API, MCP, forms, templates, automations (whose steps that set only it are skipped) and
    property access rules. Deleted properties and views are deleted permanently with the trash, after
    the workspace's trash retention. The audit log records them deleted, restored and deleted
    permanently. Migration 0051.
  • Date ranges and times. A date can have an end ("Oct 12 → Oct 14") and a time of day
    ("Oct 12, 14:30", "Oct 12, 14:30 → 16:00"), switched on in the date picker with "End date" and
    "Include time". Times are kept as instants and shown in each viewer's time zone. Filters match a
    range on every day it covers, sorts and groups go by where it starts, rollups and the formula
    functions dateStart and dateEnd read both ends, timelines draw a range from its own end
    when no end property is set, reminders come before the start (at its time, for dates with a
    time), and calendar subscriptions, CSV and Markdown exports, CSV imports and imports from
    Notion carry both. The API and MCP keep returning a plain YYYY-MM-DD for a single day and take
    start/end strings or {start, end} for the rest. No migration: existing dates keep their
    meaning.
  • Calendar weeks and events over several days. A date over several days is a bar across them
    in a calendar's month, and a calendar can show a week instead (Month / Week in its header,
    remembered by the view): dates with a time sit on its hours, all-day ones and times over several
    days in a strip above. Clicking an hour adds a row at that time, and dragging a row moves its
    whole date, both ends. MCP: calendar_mode on create_database_view and
    update_database_view. No migration.
  • Formulas over linked rows. prop("Tasks") gives a relation's linked rows, and
    prop(row, "Hours") reads a property of one: sum(map(prop("Tasks"), prop(current, "Hours"))),
    filter(prop("Tasks"), prop(current, "Done")), find, some, every, first, last, at,
    sum and average work on them, and current stands for each item. A relation alone still reads
    as its titles. Formulas read one step away (a linked row's own relations read as titles; a
    linked formula that reads linked rows itself can't be read) and up to the first 200 linked rows,
    loaded in one batch per database. They read only what you may see there: linked rows you can't
    open aren't in the list, and a formula reading a value hidden from you (or a property you don't
    know of) shows nothing, in sorts, filters, MCP and exports too. Published pages leave such
    formulas out, like rollups. The formula editor lists the linked database's properties under each
    relation. No migration.
  • Styled formula results. style(value, "b", "red") shows a result bold (b), italic (i),
    underlined (u), struck through (s), as code (c) or in an option color, as text
    ("blue") or background ("blue_background"); unstyle() removes it. Joined text keeps each
    part's style. Styles show in tables, board, gallery, list and calendar cards, the timeline,
    row pages and published pages, in light and dark; sorting, filtering, grouping, exports and MCP
    use the plain value. No migration.
  • Turn a table into a database. A table block's menu (the handle beside it) has "Turn into
    database" for people who may edit the page: a database is made under the page, as an inline
    database is, and takes the table's place in one undo step. The first row names the properties
    (the first column holds the rows' names, the others become text properties; blank names become
    "Column N" and repeats are numbered, as in CSV imports), and every other row becomes a row with
    its cells as plain text. A merged cell's text goes to its first column; the cells it covers stay
    empty. A table with only a header row makes an empty database. Undoing brings the table back
    and leaves the database under the page. No migration.
  • Hide or show every property at once. The properties list of a view's toolbar has "Hide all"
    while any property shows and "Show all" once none does, in every kind of view. The Name column
    always shows. No migration.
  • Hide properties on row pages. Clicking a property's name on a row page (or "On row pages" in
    a table column's menu) chooses whether it shows on the database's row pages: always, only when
    the row has a value for it, or never. It is set for everyone; the properties a row page leaves
    out are listed under "N more properties" below the others, still editable. Published rows and
    their print show only the properties that aren't left out. Kept when the property's type
    changes. No migration.
  • Numbers as a bar or a ring. A number property's "Number format" menu has "Show as": the
    number, a bar or a ring, with a color and the value a full one stands for ("Divide by": 100, or
    100 % for a percentage, unless set). Table cells, board and gallery cards, list rows, row pages
    and published pages show it, with the formatted number beside it; stored values, sorting and
    calculations don't change. Rollup bars and rings are drawn the same way. No migration.
  • Colors from the slash menu. A "Colors" group at the end of the slash menu colors the current
    block's text or background in the formatting toolbar's palette ("Red text", "Blue background",
    and back to the default), found by the color's name in the interface language or in English.
    Blocks that take no color (code, images, databases) don't list them. No migration.
  • Code block languages. A code block's language is picked from a menu at its top (plain text
    and 42 languages, from Bash, Go and Python to SQL, TypeScript and YAML), and its code is colored
    in that language, in the editor, on published pages and in the print view, in light and dark.
    Fences written with another name for a language (```sh, ```py) are read as
    it; a language the menu doesn't list stays as written, uncolored. No migration.
  • Search in a database view. The magnifier in a view's toolbar opens a search box that narrows
    the rows to those whose name or values hold every word typed: option names, people, linked rows,
    checklist items, file names, text, numbers, links and formula results. Case, accents and the
    Turkish dotless ı don't matter. It is your own, for whichever view is open, and isn't saved;
    values hidden from you aren't searched. No migration.
  • "Assigned to you" on the home page. Above the recently edited pages, the database rows a
    person property assigns to you and that aren't done, from every database in the workspace,
    grouped as overdue, today, the next 7 days, later and no date (by the row's first date property,
    in your time zone), each with its database and status. Only rows you can open count, and only
    where you may see the person property that names you; a status or date hidden from you neither
    shows nor leaves the row out. The 25 most pressing are listed, with the total when there are
    more. The section is hidden while nothing is assigned to you. No migration.
  • Date reminders. A date property can remind people of its dates: on the day, or 1, 2 or 7
    days before, at 9:00 in the time zone of whoever turned it on (the property's menu → Date
    settings). The people of the row's person properties, or whoever added the row, get it in the
    inbox, by email and as a push, when they can see the row and the date; rows that are done, in
    the trash or templates are skipped. MCP: date_options on add_database_property and
    update_database_property. Migrations 0049 and 0050.
  • Relative dates. A date property can show its dates as "Tomorrow", "In 3 days" or "2 days
    ago" (up to a week either way), with the full date on hover. No migration.
  • Snoozing notifications. An inbox item can be put off for an hour, until tomorrow morning or
    until next Monday; it comes back unread then. Push notifications have a "Snooze 1 hour" button.
    Migration 0049.
  • Calendar subscription. A calendar view gives each person a secret address for their
    calendar app, with the view's rows as events (at their times, else all day) as that person sees them, checked about
    every hour. Shown once; it can be replaced or turned off, and it stops while the workspace's
    export or connected apps are off. Migration 0049.
  • Quick add. A new row's name can carry its date, @people and #options ("Send the offer friday @ayşe #urgent"), in the interface language or English; what's recognised shows under the
    name and can be kept in it instead. No migration.
  • Quick notes and sharing to Leafdesk. The installed app's icon offers a quick note, and
    phones that support it list Leafdesk in the share sheet: shared text and links open the quick
    note, saved as a private page in the workspace you pick. What was shared is posted to the app,
    so it never shows up in an address or a server log. No migration.

Changed

  • Automation runs take turns by database, and at most 20,000 wait per database. The worker
    took runs strictly oldest first, so one database's bulk edits (each row times each automation)
    held back every other database's automations until they were done, and nothing bounded how many
    could pile up. It now takes the oldest waiting run of each database in turn. A write that would
    leave more than 20,000 runs waiting for a database's automations starts only the first ones and
    the server log says how many it left out; the write itself is saved as before. No migration.
  • No default password for the bundled database. docker-compose.yml no longer falls back to
    the password leafdesk for the bundled PostgreSQL, which a clone also publishes on the host's
    loopback for pnpm dev: while the app connects to it, POSTGRES_PASSWORD must be set in .env,
    as BETTER_AUTH_SECRET must. No migration.
  • Visitor addresses without TRUSTED_PROXIES. Left unset, the app now trusts
    X-Forwarded-For from one proxy only when the request comes from a private address (loopback, a
    private network, as a proxy on the same machine or Docker network does). A visitor reaching the
    app's port straight from the internet is counted by its own address, so a header it writes no
    longer gets it past the per-address limits of public forms, uploads and sign-in or into the audit
    log as someone else. docker-compose.yml no longer sets 1 itself. A number you set is trusted as
    before, for every request. The server log says at start how visitor addresses are read. No
    migration.
  • Page changes in the audit log. The audit log also records pages and databases being created
    (also by duplicating, from a template or from the web), imported (once per import, with how
    many pages it brought in), moved to another page or teamspace, put in the trash and restored.
    Database rows and the pages of templates are content and stay out of it, as edits do; a row
    deleted for good is still recorded. A page recorded before it had a title is listed under the
    title it has now. The "Deleting and publishing" filter is now "Pages". No migration.
  • Credentials masked in what agents, connections and automations keep. Text that looks like a
    credential is kept as [redacted] in an agent run's steps, answer and error; in the input of a
    call waiting for approval, as owners see it in their inbox, in the agent's runs and over MCP
    (approved, the call is still sent as the agent made it); in a connection event's body as the
    runs it starts keep it; in a connection's error and an automation step's error; in the audit
    log's details; and in what the server logs about connections, agents, automations and AI
    provider errors. Masked are passwords in addresses (https://user:[redacted]@host),
    credential-named query parameters and fields (token, access_token, api_key,
    client_secret, password, an OAuth code in an address…), Authorization headers and
    Bearer/Basic tokens, private keys, and the token formats of Leafdesk itself, GitHub, GitLab,
    npm, Slack, AI providers, Stripe, AWS, Google and JSON Web Tokens; ids and ordinary words are
    left alone. Pages and rows are not touched, and a webhook sends the row as it is. The model
    still reads tool answers and its own conversation unmasked (an event's body reaches it masked),
    and a run waiting for approval keeps its conversation as it was until it ends. Already stored
    runs, events, errors and audit events stay as they are. Masking takes time in proportion to the
    text's length, however it is made up. No migration.
  • A clearer home page. The home page greets you with today's date under the welcome, and
    shows the recently edited pages as cards in two columns, with titles up to two lines instead of
    cut off. The assigned section now says so when nothing is assigned to you instead of
    disappearing. No migration.
  • Allowed email domains are proven by DNS. Anyone could add a domain they don't own to a
    workspace's allowed domains and so let in, or be found by, everyone signing up with it. A domain
    now counts only once its owner adds a TXT record (the same kind single sign-on asks for) and an
    owner clicks Verify; a domain one workspace has verified, for single sign-on or as an allowed
    domain, can't be verified by another. The audit log records it. No migration.
  • Adding someone by email needs a verified address. Anyone can sign up with someone else's
    address, so adding a member by email, sharing a page by email, approving a member's invitation
    request, MCP's invite_member and SCIM provisioning only add an existing account whose address
    is verified. Others get the invitation emailed to the address, as someone without an account
    does ("invited", not "added"); SCIM answers 400 until the address is verified. Approving
    someone's own request to join no longer hands them the invitations waiting for an unverified
    address. No migration.
  • A verification link hands the account to the address's owner. Following it in a browser
    that isn't signed in to the account ends its other sessions, removes its passkeys, two-step
    verification, API tokens and connected apps, and makes its password be replaced through an
    emailed reset link, so whoever signed up with the address first keeps nothing. Following it
    where you signed up, still signed in, changes nothing. No migration.
  • Leaving a workspace takes what it gave. Leaving, being removed or being removed over SCIM
    now also deletes the person's page and property entries there, their calendar feeds of it and
    API tokens made only for it (after an owner takes over the pages only they could manage), so
    coming back starts afresh. Migration 0056 clears what earlier departures left.
  • File addresses in a page grant nothing by themselves. Writing a file's address into a page
    let everyone who could read the page open the file. A file now loads on a page only once someone
    who can already open it puts it there (by editing, the API, MCP, a database value or a
    template); duplicates, templates and published copies keep that. A file held only in a files
    property that property access hides is no longer served through that row, to visitors of
    published pages either. References made before the upgrade keep working. Migration 0057.
  • Less in the Share panel for guests. Access inherited from a page above names that page only
    to people who can open it ("From a page above"), and guests see people's names but not their
    email addresses, and that a group has access but not which one. No migration.
  • Published databases send less. Visitors get each property's name, type and what drawing its
    values takes, not its whole settings (formula expressions, relation and rollup settings). No
    migration.
  • Notices name a database only to people who can open it. Inbox items and assignment,
    reminder and automation emails leave out the database's name for someone who can open the row
    but not its database. No migration.
  • Event runs ask before acting. Every connection tool call of an agent run started by a
    connection event waits for an owner's approval, unless its trigger is marked "Trust what this
    service sends". Owners approve the call exactly as it will be sent: the run shows it in full and
    unmasked, and the inbox offers Approve only when its shortened view is the whole call, else
    "Open the run to approve". Answering an agent's call holds owners to the workspace's two-step
    and single sign-on policies. A run started by an answer to a public form from someone outside
    the workspace acts with no more access than the form's publisher has. Migrations 0053 and 0054.
  • AI autofill leaves out rollups over restricted values. A rollup over a related property
    that property access restricts, and formulas reading such a rollup, are no longer given to the
    model. No migration.
  • Formulas stop when they take too much work. A formula gets 100,000 steps per row, map()
    and other lists at most 10,000 items, and replaceAll() and formatDate() results at most
    10,000 characters; past that, or on any other evaluation error, that formula shows "takes too
    much work" (or an error) on that row and the rest of the page loads. Nested list functions over
    linked rows could otherwise keep the server busy for minutes. No migration.
  • Pages nest at most 60 levels deep. Creating, moving, duplicating, database rows, templates
    and imports refuse deeper pages, and the access rule gives no access to a page it can't follow
    to the top level, rather than the teamspace's default past a restriction higher up. Migration
    0055.
  • No sign-in provider tokens are kept. Signing in with GitHub, Google, OIDC or single sign-on
    no longer stores the provider's access, refresh and ID tokens, and those stored before are
    cleared. SECURITY.md lists what is still stored as it is. Migration 0055.
  • Emoji icons only. Page and teamspace icons take one emoji (up to 8 code points); creating a
    page from the app takes only the fields the app sends, with content up to 500,000 characters.
    No migration.