⊼

heracl.es

/listula

A local-first, multiplayer grocery and task list, as a web app.

Listula is a shared checklist that runs in a web browser, mostly used for groceries and small task lists. People share a list by exchanging a short code. There are no accounts, tracking or analytics. Lists are encrypted on the device, so the server stores only data it cannot read. The app works offline and can be installed to a phone’s home screen.

The design is deliberately plain: black ink on white paper, in a handwritten typeface, with no labels, icons or help text. The only words on the sheet are the ones its users wrote.

Name

The name is the diminutive of lista, medieval Latin for “list”, so it means “a little list”.

Use

A list is a sheet of tasks arranged in groups. Tasks can be written, ticked off, reordered or moved between groups. Whole groups can be folded away, cleared or moved to another list. Quantities and prices written into a task are read from the text and totalled for the group, but the text itself is stored unchanged.

Sharing

A list gets its code the first time it is synced, from the menu at the top right. The code is twelve characters long. SHARE sends a link and the code together, and COPY copies the code alone. The recipient enters the code under JOIN. Joining never discards anything: the device’s existing tasks either go into the joined list or stay where they are, as a separate list.

The code is never placed in a URL. The link ends in ?j, which only tells the app that the visitor was sent a list. On that one visit, red marks show where the code goes. They disappear once the code is entered or the visitor does anything else.

Syncing is manual. Tapping SYNC sends local changes and fetches everyone else’s. Nothing is sent automatically.

Gestures

Nothing on the sheet is labelled, so this section is the complete list of what the app responds to. A long press is about half a second, and a double tap is two taps within about a third of a second.

If this section and the app ever disagree, the app is right and this section is out of date.

Tasks

WhereGestureResult
CheckboxTapTo-do ⇄ done
CheckboxDouble tap, or holdHalf done; holding a half-done task resets it
CheckboxSpace / ⇧SpaceThe same two
WordsTapOpens the task for editing, with the caret there
WordsDouble tapDone
WordsTriple tapHalf done
WordsFourth tapBack as it was, open for editing
WordsPull leftDone
A done taskPull left againDeletes it, with an undo
A done taskTap the scribbleDeletes it, with an undo
WordsHold, then dragPicks the task up, also into another group
WordsF2Opens the task for editing
Any partAlt+↑ / Alt+↓Moves it, crossing into the next group at the ends

Tapping near the end of a task only opens it, because that is usually someone about to add to it. Inside a task that is already open, taps just place the caret.

Editing a task

GestureResult
EnterSplits the task at the caret; the rest moves to a new task
Enter at the startOpens an empty task above
Backspace at the startJoins the task onto the one above, if both fit in one task
Backspace on an empty rowRemoves the row and moves to the task above, or to the group name
EscapeDiscards the edit
Typing past 200 charactersThe task fills up and the rest starts a new one

A countdown of the characters left appears in the last ten.

Groups

WhereGestureResult
TitleTapFolds or unfolds the group
TitleDouble tap, or F2Opens the name for editing
TitleShort holdOpens the name; a buzz says when to let go
TitleLong hold, then dragPicks up the whole group; all groups fold meanwhile
Fold iconTapFolds or unfolds the group
Fold iconHoldFolds every group, or unfolds all if all are folded
Name fieldEnterSaves; on an empty group, also opens its first task
New-group …EnterCreates the group and opens its first task
Carried groupDrop on the list nameOpens the other lists; drop on one to move it there
Carried groupDrop on + NEW LISTCreates a new list containing the group
Carried groupDrop on the cornerDeletes the group and its tasks, with an undo

A folded group’s icon reads (1/3): tasks still to do, out of the total. With nothing done it reads (3).

The scribble beside a group appears only while its name is open or the group is folded. It clears the done tasks, or deletes the group once everything in it is done. Dragging a task onto the new-group row creates a group for it.

A list that a group was moved to is marked with an asterisk until the tab is closed.

The sheet and the menu

WhereGestureResult
SheetDrag right on bare paperTurns the paper over to the menu
MenuDrag right anywhereTurns it back
BurgerTapTurns the paper over, from either side
BurgerHoldTurns debug mode on
Sync markTapSyncs; the mark appears only when there is something to report
List nameTapShows the other lists and + NEW LIST
List nameDouble tapOpens the next list directly
Theme markTapSwitches between the opposite of the phone’s theme and the same
Code fieldTap while emptyPastes a code from the clipboard
A messageTap UNDO?Undoes what the message describes
A messageFlick up or rightDismisses it
A panelDrag down, Esc, or ✕Closes it

Anything that removes something leaves a message with an undo for ten seconds, and so does moving something by hand. LEAVE removes a list that has a code from this device only. The same button reads DELETE when the list has no code, because this device is then its only copy. Both ask first and can be undone.

Ticking three tasks within five seconds offers to clear those three. It is only an offer and is made once per run.

Debug mode outlines every element in red, keeps a log of sync attempts and adds a language picker. A button in the menu turns it off.

What the sheet reads

A number at the start of a task is a count (2x apples, 3 lemons). A number with a metric unit is an amount (500g, 1,5 kg, 2L, 250ml), and its price is not multiplied. Either can be a rising range (2-3 oranges), and a range is totalled at its top. A number at the end is a price (Bread 2,50). Both , and . work as decimal marks.

The group total is what is still to buy: done tasks are left out, half-done ones count in full, and each price is multiplied by its count. Web and email addresses become links that open in a new tab. None of this changes the stored text.

Limitations

  • The code is the only key. There are no accounts and no recovery. If the code is lost, so is the shared list. EXPORT copies the list as Markdown and is the only backup.
  • Access cannot be revoked. Anyone with the code can read and change the list. LEAVE removes it only from one device. If a code leaks, everyone has to move to a new one.
  • Unsynced changes stay on the device. They reach nobody else and are lost with the device. An arrow in the corner shows when changes are waiting.
  • Unused lists expire. A list not edited for six months is deleted from the server. Reading it does not count.
  • Encryption does not protect against the host. It protects stored data, not against whoever serves the app’s code.
  • The first screen is in English. The page is built in advance, so a phone set to Greek briefly shows English before switching.

Deployment

  • Keep the Vercel Blob store private. Only the server needs to read it.

  • Add CRON_SECRET by hand (openssl rand -hex 32) as a private environment variable. Without it, the daily cleanup of expired lists never runs. Never commit it or give it a PUBLIC_ name.

  • To check that a deployment can write, set HOST to the deployment’s domain and run:

    HOST=example.vercel.app
    ROOM=$(openssl rand -hex 16)
    curl -i "https://$HOST/api/room/$ROOM"
    curl -i -X PUT "https://$HOST/api/room/$ROOM" \
      -H "Authorization: Bearer $(openssl rand -hex 16)" \
      -H 'Content-Type: application/json' -d '{"baseV":0,"blob":"AA=="}'
    

    A healthy deployment answers 404, then 200 {"v":1}. A 500 on the second call means the store is not connected. The check writes one tiny empty list under a random address, which no code can open and which is deleted after six months.

  • The server limits each address to 120 reads and 30 writes a minute. The count is kept per server instance, so for a hard limit also add a rate-limit rule for /api/room/ in the Vercel firewall.

Typeface

The app uses one typeface, Graphe Alpha, drawn by the project’s author and released under the SIL Open Font License. It has no [, ], \ or Ϋ, so those characters fall back to another face. Greek text is marked as Greek so that capitals drop their accents, as Greek spelling requires.


Dialectic Acheropoieton
of Heracles Papatheodorou and Claude