Repository navigation
Leafdesk 0.5.1
Upgrading from 0.5.0
-
Migrations (0049–0059) run automatically when the container starts.
-
Docker Compose: download the new
docker-compose.yml. It needsPOSTGRES_PASSWORDfor the
bundled database (see below) and passesAUTOMATION_DATE_CHECK_SECONDSto 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_PROXIESbehind 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), setTRUSTED_PROXIES=1(or2for 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_PASSWORDis required for the bundled database. Download the new
docker-compose.yml: withCOMPOSE_PROFILES=bundled-dband noEXTERNAL_DATABASE_URL, it
refuses to start until.envsetsPOSTGRES_PASSWORD. If you never set one, your database
has the old default: addPOSTGRES_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 fromBETTER_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 takeopen_rows(panelorpage);get_database
showsopen_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_zoneover 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
aspage.trashedby 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_passedwith thatdate; notifications say the row's date has come. MCP's
create_automationandupdate_automationtake both and atime_zone,list_automations
describes them and shows each automation'stime_zoneand a paused one'scheck_error,
list_automation_runsgives each run'sdate.
Migration 0058 adds theautomation_firetable,automation_run.basis,
database_automation.time_zoneand.check_error, and the audit actor kind. Covered by
scripts/timed-automation-e2e.tsand 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 withlist_row_activity, the REST API atGET /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/healthanswers 200{"status":"ok"}whenever the server
handles requests, without reading a session or the database.GET /api/health/readyalso 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'sHEALTHCHECKuses/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
.docxfiles, 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.docfile 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
functionsdateStartanddateEndread 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 plainYYYY-MM-DDfor a single day and take
start/endstrings 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_modeoncreate_database_viewand
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,
sumandaveragework on them, andcurrentstands 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_optionsonadd_database_propertyand
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,
@peopleand#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 passwordleafdeskfor the bundled PostgreSQL, which a clone also publishes on the host's
loopback forpnpm dev: while the app connects to it,POSTGRES_PASSWORDmust be set in.env,
asBETTER_AUTH_SECRETmust. No migration. - Visitor addresses without
TRUSTED_PROXIES. Left unset, the app now trusts
X-Forwarded-Forfrom 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 sets1itself. 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 OAuthcodein an address…),Authorizationheaders and
Bearer/Basictokens, 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'sinvite_memberand 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, andreplaceAll()andformatDate()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.