---
name: sliprail-app
description: "Building or changing a Sliprail app, a directory or project whose sliprail.json has an app section."
---

# Sliprail app conventions

Apps open through a link, inside the Sliprail app or in a browser. Follow these conventions, which come from how Sliprail hosts them, together with the usual practices of the project.

## The app directory

An app is a directory with the entry page at index.html in its root, relative links between pages and assets, and sliprail.json next to index.html. Write plain HTML, CSS and JavaScript, or build the app into such a directory before publishing it. The platform adds a script that provides window.sliprail, so do not include one.

## sliprail.json

```json
{
  "displayName": { "en": "Expenses", "zh-hans": "记账" },
  "icon": "icon.svg",
  "dataLists": [],
  "app": {}
}
```

- displayName is the concise title of the app: a string for an app in one language, or a map with a text for each of its languages.
- icon is the path of a square image among the files of the app, such as icon.svg.
- dataLists declares the cloud data of the app, app.dataAccess other data it needs, app.capabilities abilities such as ai, as described below.
- id identifies the app, its link and its data. The first publish assigns it when sliprail.json has none; keep it unchanged afterwards.

## Layout

When opened in the app, the page fills the screen up behind the status bar, with the app's controls in the top-right corner below it.

- Keep content clear of the status bar with padding of var(--sliprail-safe-area-top, env(safe-area-inset-top)) at the top, including on sticky or fixed headers.
- Keep about 96 by 40 pixels in the top-right corner below that free of interactive elements.
- If the top edge or that corner shows an image, video, canvas or gradient, mark the element with data-sliprail-capsule="light" or data-sliprail-capsule="dark" to match how it looks, and update it when the look changes, such as when the theme changes.

## Languages

Apps can use the languages en, zh-hans. People pick the language from the app's controls, so do not add a language switcher.

- List the languages on the html element, the default first, such as <html lang="en" data-sliprail-languages="en zh-hans">, and give displayName in sliprail.json a text for each of them.
- When the app starts, show it in the language its lang attribute holds. Picking another language reloads the page, so reading it once at startup is enough.
- Keep the texts of every language in one messages object keyed by these identifiers with the same keys, and show them through a t(key, params) function, so that every visible text, including the page title, is translated.
- Format dates, numbers and plurals for the current language.
- Keep data language-neutral, such as currencies and keys of preset items, and translate only what is shown.
- When the app asks AI for text, ask it to reply in the current language.

## Light and dark themes

People pick the theme from the app's controls, so do not add a theme switcher. The theme can change while the app is open.

- Put data-sliprail-theme="light" on the html element; the attribute then holds the theme to show.
- Style the light look by default and the dark look only under html[data-sliprail-theme="dark"].
- Redraw content that CSS does not restyle, such as canvas drawings and charts, when the attribute changes.

## Cloud data

An app keeps whatever people add, change or save in the lists declared in dataLists of sliprail.json by default, even when the request does not mention it, so they find the same data in any browser or on any device; keep data only on the device when the user asks for that. No permission prompt is shown for an app's own data.

- List and field names use letters, digits and underscores, list names are lowercase.
- Fields can only be added in later versions and never renamed, removed or retyped, so add a new field instead of changing one.
- Assistants read the lists through their descriptions, so describe in each list's description what its fields mean and any unit or format that could be misread, such as amounts kept in cents or dates kept as YYYY-MM-DD.
- To use the data, call window.sliprail.data: sliprail.data.lists() lists the declared lists; sliprail.data.list(name) returns find(options), findAll(options), count(options), get(id), add(itemOrItems), update(itemOrItems), remove(id) and removeMany(ids), all returning promises.
- find options are filter ({condition, parameters}), sort, order, size and after; it resolves to {page, after} where after is the cursor of the next page.
- update only changes the fields passed with the record id. Records also carry id, createdAt and updatedAt.

## Sharing data

Each list is private to each person using the app by default.

- For data people keep together with others they invite, such as a family ledger, set the list's scope to group, manage groups with sliprail.groups: list(), create({name}), rename(group, name), remove(group), invite(group, emails), members(group), removeMember(group, email) and leave(group), and open a group's list with sliprail.data.list(name, {group}).
- Invited people are notified and join once they accept the invitation in Sliprail; until then members(group) lists them with pending set to true and they cannot open the group's lists. Members can change only the records they added, while the group's creator can manage all of them.
- Use scope shared only for data everyone using the app shares, such as a leaderboard.
- Records of group and shared lists carry createdBy, which equals sliprail.data.me() for records the current person added.
- Let people know in the app what others can see. A list's scope cannot change later.
- To let people share a single record of their own as a read-only text page with anyone who has the link, set "shareable": true on a list with the user scope, then call list.share(id, {type: 'text', title, text}) to get {url}, list.getShare(id) to get the current {url} or null, and list.unshare(id) to stop sharing. Removing a record also stops sharing it.

## Other data

To read or write other data of the people using the app, such as their assistant data, data synced from their devices or another app's data, declare it in app.dataAccess of sliprail.json; each person is asked to allow it the first time they use the app, then use sliprail.data.assistant, sliprail.data.synced or sliprail.data.app(appId), which offer the same lists() and list(name) as sliprail.data.

## AI

To generate text with AI in the app, add "ai" to app.capabilities of sliprail.json; each person using the app is asked to allow it the first time, and it uses their AI credits.

- Call window.sliprail.ai.request({input, instructions, modelTier, tools, tag}), where input is a string or a non-empty array of {type: "text", text}, {type: "file", mimeType, filename, source} and {type: "tool-result", id, name, result} parts, modelTier is fast, balanced or advanced, tools are {name, description, parameters} with a JSON schema object, and tag groups the saved conversation like the tag of createSession.
- To ask about an image, PDF, text file or MP4 video picked in the app, pass a file part whose source is {type: "inline", data} with the File, Blob, ArrayBuffer or Uint8Array, or {type: "url", url} for a file the app can fetch; each file can be up to 10 MB, and an MP4 video up to 50 MB. Without a filename, the name of the File or the mimeType decides the file type.
- It returns an async iterable: read it with for await, append the text of {type: "text", text} chunks as they arrive, run {type: "tool", id, name, arguments} chunks and send their results in a later request, and stop at {type: "done", reason}, where reason is completed, tool, cancelled or error with error. Pass signal to cancel.
- For a conversation that remembers earlier turns, use sliprail.ai.createSession({title, instructions, modelTier, tag}) and session.request({input, tools}); sliprail.ai.getSession(id), listSessions({tag}) and deleteSession(id) manage saved sessions.

## Errors

Show the error message when a request fails, for example because permission was declined or cannot be granted where the app is opened.

## Publishing

Publish with the tools of the Sliprail MCP server at https://sliprail.com/_api/sliprail/mcp. If they are not available, ask the user to connect it as described in https://sliprail.com/docs/developer/connect-your-agent; it needs an access token with Sliprail publishing access, created at https://account.fengcen.com/create-access-token.

1. Pack the package directory, the one with sliprail.json at its root, into a gzipped tarball outside of it, leaving out node_modules and hidden files:
   - macOS and Linux: `tar -czf "${TMPDIR:-/tmp}/sliprail-package.tgz" -C <package directory> --exclude=node_modules --exclude='*/.*' .`
   - Windows PowerShell: `tar.exe -czf "$env:TEMP\sliprail-package.tgz" -C <package directory> --exclude=node_modules --exclude="*/.*" .`
2. Read the size of the tarball in bytes with `wc -c < "${TMPDIR:-/tmp}/sliprail-package.tgz"`, or `(Get-Item "$env:TEMP\sliprail-package.tgz").Length` in PowerShell.
3. Call create_package_upload with the size, run the upload command it returns as it is, then call publish_package with the returned uploadKey.
4. When publish_package returns an id that is not the id in sliprail.json, or sliprail.json has no id, write the returned id into sliprail.json and keep it, so that later publishes update the same package and its data.

publish_package returns the link of the app, which stays private to the user and is added to their apps. window.sliprail is only available in the published app, so try the app through its link. To change a package published earlier, get its files with get_package_files, edit them and publish them again. Never include secrets or private data in the files.

## Checklist

- sliprail.json describes the app and sits next to index.html.
- Content stays clear of the status bar and the top-right controls, and the capsule tone matches what is shown there.
- Every visible text, including the page title, is shown in each declared language, and dates, numbers and plurals follow it.
- Both themes look right, including after switching while the app is open.
- There is no language switcher or theme switcher.
