Home  ›  tafalo5 — Guide
v2 · 2026-09

Guide to tafalo5

tafalo5 (codename tfl5) is a multi-tenant app platform: you declare your data (resources), store records (docs), control who sees what with ACLs, publish a front-end on your own domain, and extend it with hooks and sandboxed code. This page is the overview; the full reference and SDK live in the public repository.

2. Model & roles

Everything an app stores is built from five objects. Every request is checked against ACL arrays on those objects.

user                (an account, shared across apps)
group               (platform-wide set of users)
app                 (your workspace: members, ACL, domains, front-end)
 ├── role           (a named set of members, used in the app's ACLs)
 ├── resource       (a typed collection: fields, hooks, its own ACL)
 │    └── doc       (one record)
 └── files / site   (uploaded files and the published front-end)

2.1 Permission levels

LevelComes fromCan
Ownerthe app's creator (or whoever it was transferred to)everything, including deleting the app, billing and unbinding domains
Managermanagersapp ACL (except managers, which only the Owner changes), roles, resources, site publishing, domains
Designerdesignersmembers and direct grants
Editoreditorscreate and change data and files
Readerreadersread
Note. A level is membership in an ACL array, per app. You can be a Manager of one app and only a Reader of another.

3. Quick start — 5 steps

  1. Register an account and confirm your email (the link arrives by email).
  2. Sign in — the server sets a session cookie.
  3. Create an app — you become its Owner.
  4. Create a resource and your first docs.
  5. Publish a front-end and, optionally, bind your own domain.
Tip. Everything below is available from code through the SDK (§28). The step-by-step version is in Getting started.

4. App

An app is an independent workspace with its own members, roles, resources, files, front-end and domains.

  • The creator is the app's Owner. Ownership can be transferred to another user.
  • How many apps you can own depends on your plan (§23).
  • Creating an app needs a confirmed email address.

5. Resource

A resource is a typed collection: its fields (each with an optional validator and an encryption level), its hooks, and its own ACL.

  • Level 0 fields are stored in plain text and can be filtered on.
  • Level 1 and 2 fields are encrypted at rest. Clients read and write plain values; filtering on them is refused.

6. Doc

A doc is one record of a resource. Besides its data it carries an author and its own ACL arrays (editors, readers, deletable, noaccess).

Reading is decided above the doc. Whether someone can read a doc is decided by the app, the resource and scope bindings. A doc's own arrays govern who can change or delete it, not who can read it. To keep records apart, put them in separate resources with their own resource ACL (§21). Scope bindings (§20) only take effect on servers where the operator has turned scope enforcement on; it is off by default.

7. Files & sites

An app has two kinds of files: files its users upload (with their own ACL), and its front-end. The front-end is published through the site engine: you edit a draft, publish it as a snapshot, and can roll back to a recent one. Older snapshots are pruned (by default, those beyond the 20 most recent that are also older than 90 days).

8. Domain

A custom domain is bound to an app after a DNS check. One app can serve several domains, up to its plan's domain limit.

9. Groups & roles

A role belongs to one app; a group is platform-wide and managed by the platform operator. Both are used by putting their token in ACL arrays.

  • u-<uuid> — one user.
  • g-<uuid> — a group.
  • [r-<uuid>] — a role. The brackets are part of the token: /app/acl-set, /app/doc/acl-set, /app/file/acl-set and resource create/update add them to a raw r-<uuid>, but doc create, update, upsert and create-batch store the ACL arrays as sent, so bracket role tokens yourself there.
  • G_author — every signed-in user.

10. Workflow — Register & sign in

10.1 Register

POST /reg with {username, password, re_password, email}. The server emails a confirmation link; until it is followed the account can use apps it was invited to but cannot create apps. If the server has bot protection enabled, a Turnstile token is also required.

10.2 Sign in

POST /login with {username, password} sets the session cookie:

HttpOnly Secure SameSite=Lax
  • The cookie value is encrypted; page scripts cannot read it.
  • A session lasts at most 24 hours from sign-in; activity does not extend it.
  • Sign-in endpoints are rate-limited per IP address.
  • Other methods: Google, Microsoft, email link, phone code, QR code from a signed-in phone, Telegram, VNeID — see Authentication.
Security note. A wrong username and a wrong password return the same message.

10.3 Change password

POST /user/change-password with the current and the new password. Changing it signs out every other session; the current one gets a fresh cookie.

10.4 Sign out

POST /logout removes the session cookie from this client; afterwards POST /user answers {isSignout: true}. The cookie value itself stays valid until it expires (24 hours from sign-in), so to end every session — for example after a device is lost — change the password (§10.3).

10.5 Delete account

POST /user/data/erase schedules erasure after a grace period (it can be cancelled in that window). It is refused while you still own apps — transfer or delete them first. POST /user/data/export returns your account record, metadata about your email addresses (the addresses themselves come from /user/email/list) and the apps you are a direct member of. Docs and files inside an app are exported through that app's own /app/doc/* and /app/file/* calls.

11. Workflow — Create an app

  1. Prerequisite: a confirmed email and a free app slot on your plan.
  2. Send POST /app/update with {data: {name, description}} (no tid = create).
  3. The server returns the new tid (a-<uuid>); you are its Owner.
RefusalCauseWhat to do
HTTP 402, code quota_exceededNo app-creation right leftIf data.refundable_on_delete is true, delete an app you no longer need; otherwise buy more app-creation rights or move to a larger plan (deleting an app does not give a right back).
code email_not_verifiedEmail not confirmed yetFollow the confirmation link (it can be sent again).

12. Workflow — Bind a custom domain

  1. Preview: POST /app/domain/preview {app_tid, domain} returns the DNS records to create.
  2. Configure DNS: an A record pointing at the platform, or a TXT record at _tfl5.<domain> — either one proves ownership.
  3. Add: POST /app/domain/add {app_tid, domain} (Manager). If neither the A record nor the TXT proof is found, the call is refused and data lists the records to create. On success, warnings flags a domain proven by TXT that does not point at the server yet.
  4. TLS: the certificate is issued automatically on the first HTTPS request.
  5. Re-checks: DNS is re-checked about once a day; after three failures in a row the domain is marked inactive.

Unbinding a domain needs the app Owner. The owner of a parent domain can also let other apps use its subdomains.

13. Workflow — Design a resource

Creating a resource needs Manager on the app. The body is flat (the // comments are annotations — remove them before sending, JSON has no comments):

POST /app/resource/create
{
  "app_tid": "a-…",
  "ma": "post",                         // unique alias within the app
  "name": "Blog post",
  "fields": [
    { "field": "title",  "validator": "required,minlen:3" },
    { "field": "body" },
    { "field": "note",   "level": 2 }   // encrypted at rest
  ],
  "readers": ["[r-…]"]                  // optional resource ACL
}
Levels matter. Level-0 fields are plain text and filterable. Level-1/2 fields are encrypted; a filter on them is refused with cannot_filter_encrypted_field. Validators are one comma-separated string (required, minlen:N, maxlen:N, min:N, max:N, email, number, choice:a|b, …).

14. Workflow — Doc CRUD

14.1 Create

POST /app/doc/create
{ "app_tid": "a-…", "resource_ma": "post", "data": { "title": "Hello world", "body": "…" } }

14.2 List & filter

(Annotated — remove the // comments before sending.)

POST /app/doc/list
{
  "app_tid": "a-…", "resource_ma": "post",
  "where": { "status": ["draft", "review"] },   // exact match; an array means "any of"
  "limit": 20                                   // default 100, max 500
}

where matches level-0 fields exactly (up to 10 keys). Page with offset (up to 100 000) or, for deep lists, the cursor returned as next_cursor. Results are newest first.

14.3 Update & delete

  • POST /app/doc/update {app_tid, tid, data} replaces data: fields you leave out are removed. Read, merge and send the whole object to change a few fields.
  • POST /app/doc/del {app_tid, tid} soft-deletes the doc.
  • POST /app/doc/acl-set changes a doc's own ACL (Owner/Manager, or the doc's author).

15. Workflow — Files & publishing

  • Upload: POST /app/file/upload as multipart (app_tid, then pairs of path + file). Up to 50 MiB per file.
  • Stages: each file is in the test (draft) or release (live) stage. Uploads default to test; reads default to release. Send stage=release to write live files directly.
  • Download: POST /app/file/sign-url {app_tid, path} returns a short-lived link (5 minutes by default).
  • Trash: deleted files can be listed and restored until a Manager purges them or the trash retention (30 days by default) runs out.
  • Publishing the front-end: the site engine (/app/site/put, /app/site/publish, /app/site/rollback) keeps recent published versions for rollback; older snapshots are pruned (by default, those beyond the 20 most recent that are also older than 90 days).

Going over the app's storage allowance is refused with code quota_app_max_storage.

16. Workflow — Sharing

A share hands out a read-only link to one doc, for people without an account.

16.1 Create a link

POST /app/share/create
{ "app_tid": "a-…", "doc_tid": "d-…", "target": "anonymous",
  "fields": ["title", "summary"], "expires_at": 1893456000000 }
=> data: { "tid": "…", "target": "anonymous", "token": "<32 hex>" }

Build your own link around token. The receiving page calls POST /app/share/claim {app_tid, token} without signing in and gets the doc's level-0 fields (only fields, when given). Encrypted fields are never returned through a link.

16.2 Treat the token as a password

App Editors can list existing shares (/app/share/list), tokens included. Revoke a share (/app/share/revoke) to cut access; that takes effect immediately.

16.3 Interaction with other rules

  • A noaccess entry does not block a link — revoke the share instead.
  • Turning sharing off on a resource stops new shares; existing links keep working until revoked.
  • Deleting the doc makes its links stop working.

17. Workflow — Multiple domains, one app

Bind several domains (for example acme.com and acme.vn) to the same app: they serve the same front-end and data. The number of domains per app is capped by the app's plan; over the cap, /app/domain/add answers HTTP 402 domain_quota_reached.


18. Access control — the permission set

For every request the server works out which tokens the caller holds:

permission_set = [
  user tid,                     // "u-<uuid>"
  username,
  ...group tids,                // "g-<uuid>"
  ...role tokens in this app,   // "[r-<uuid>]"
  "G_author"                    // every signed-in user
]

A check passes when the permission set and the ACL array share at least one token. The app's Owner always passes.

19. ACL arrays

ArrayGrants
managersManager level (app). On a file ACL it only makes the file visible: changing a file's ACL needs app Manager, and deleting it needs app Owner/Manager or deletable.
designersDesigner level (app)
editorscreate and change data
readersread
deletabledelete: enough on its own for a file; on a doc it is needed in addition to editors (the doc's author and app Owner/Managers can always delete). App- and resource-level deletable give no delete right.
noaccessdeny, even if another array grants access — never the app Owner, and on a doc not app Managers or the doc's author

20. Where each layer applies

LayerSet withGoverns
App/app/acl-seteverything in the app
Resource/app/resource/create or /app/resource/updatereading and writing that resource's docs
Scope bindings/app/scope/setwhich rows a user sees, by attributes such as school or ward — only where the operator has turned scope enforcement on (off by default)
Doc/app/doc/acl-setchanging and deleting that doc
File/app/file/acl-setthat file

Reading a doc is decided by the app, resource and scope layers; see the note in §6.

21. Resource ACL

A resource has its own readers, editors, noaccess and deletable arrays. Set them when you create the resource or later with /app/resource/update (Manager); an array you leave out keeps its value. Leave them empty and only the app's ACL applies. This is the layer to use when some members must not read a resource's records through /app/doc/*. Encrypted attachments (/app/f3/*) and share links are checked against the doc and app ACL, not the resource ACL.

22. Who can change ACLs?

WhatWho
App ACLManager (only the Owner can change managers)
Roles and their membersManager
Direct grants to a memberDesigner
Doc ACLOwner/Manager, or the doc's author
File ACLManager
Groupsthe platform operator

23. Platform rules — Quotas & plans

Your account has a tier that sets how much storage you can use; each app has its own storage allowance. Creating an app spends one app-creation right (bought in packs; deleting an app does not give it back) — or, on an account that has no rights recorded, counts against the tier's user_max_apps, which deleting an app frees again. Services such as extra storage are sold as plans; prepaid credits pay for plan changes.

LimitApplies to
user_max_appsapps you can own, on an account without app-creation rights
user_max_total_storagestorage across all your apps
app_max_storagestorage of one app

The public catalog of plans and prices is at GET /billing/catalog. Plan features are display text; the numeric limits are what the platform enforces.

24. Platform rules — Security & encryption

  • Fields at level 1 or 2 are encrypted at rest with an AEAD cipher (ChaCha20-Poly1305).
  • Session cookies are encrypted, HttpOnly, Secure and SameSite=Lax, and last at most 24 hours.
  • Encrypted attachments can be bound to a doc; at the highest level only users granted access can read them, and every download can be logged.
  • Server-side code you upload (WASM operators) runs in a sandbox with fuel, memory and time limits, as the calling user, inside its own app.
  • Audit log: control-plane changes are recorded per app; a resource can opt into an audit row for every doc change.

25. Platform rules — API conventions

  • Responses: most successes are {result: true, data, timestamp} (a few, such as /login and /app/site/*, put their fields beside result). A refusal carries {result: false, msg, code}, except a missing or expired session, which answers {isSignout: true, result: true} — HTTP 401 on most endpoints, 200 on a few, and not always with a code. Branch on code and isSignout, never on msg, and do not rely on the HTTP status alone — some refusals answer HTTP 200.
  • Every data call names its app with app_tid in the body.
  • Filtering is allowed on level-0 fields only.

26. Data lifecycle

EntityDelete
DocSoft delete (/app/doc/del).
ResourceDeleting it drops its docs (Manager); refused while another resource links to it.
FileTo the trash; restorable until a Manager purges it or the trash retention (30 days by default) runs out.
AppOwner only. The first delete is soft; deleting again removes it and its data for good.
AccountErased after a grace period; refused while you own apps.
ShareRevoked at once.
DomainMarked inactive after repeated failed DNS checks.

27. Hooks & validators

27.1 Field validators

{ "field": "title", "validator": "required,minlen:3" }

A failing value is refused with code field_validation_failed.

27.2 Declarative hooks

Hooks live on the resource and run on before_create, after_create, before_update, after_update, before_del, after_del. Event names are not validated — a misspelled one never fires.

TypeDoes
require_fieldsrefuses the write unless the listed fields are present (code hook_validation_failed)
set_fieldsstamps values onto the doc after it is written
webhookPOSTs the doc to your URL in the background (plain JSON, unsigned — authenticate it with a secret in the URL); up to 5 attempts over about 75 minutes. Delivery is deduplicated per hook, doc and event, so a doc sends one after_update webhook, for its first update only — later updates of the same doc send nothing (a known server issue)
wasmcalls your WASM operator

27.3 Code guards

For logic a hook cannot express, a resource can carry short JavaScript guards (before_create_code, before_update_code, …) that run in a sandbox and can reject the write.

28. SDK & integration

FormUse
<script src="/sdk.js">any page on your app's domain; defines window.TFL5 (ESM: /sdk.mjs)
@tfl5/sdkbuild pipelines and Node 18+ (ESM); not on npm yet — build it from the sdk/ folder of the GitHub repository and install it by path

Browser

<script src="/sdk.js"></script>
<script type="module">
  const tfl5 = new TFL5();                       // same origin, session cookie
  await tfl5.auth.login(username, password);
  tfl5.useApp("a-…");
  const posts = await tfl5.resource("post").list({ where: { status: "published" }, limit: 20 });
</script>

Node

import { TFL5 } from "@tfl5/sdk";
const tfl5 = new TFL5({ host: "https://acme.com" });
await tfl5.auth.login(username, password);      // kept in an in-memory cookie jar
const me = await tfl5.auth.me();

Opening a share link

const tfl5 = new TFL5();
const shared = await tfl5.shares.claim(tokenFromYourUrl, "a-…");   // no sign-in

Full guides and the method reference: SDK documentation.

29. Common errors

codeCauseAction
unauthorizednot signed in, or the session expired — the body is {isSignout: true, result: true}, so check isSignout as well as resultSign in again.
access_deniedsigned in but not allowedAsk a Manager to grant access.
email_not_verifiedemail not confirmedFollow the confirmation link.
validation_email_takenthe email belongs to another accountSign in to that account or use another address.
quota_exceeded, quota_app_max_storagea plan limit was reachedFree space or upgrade.
cannot_filter_encrypted_fieldfiltering on a level-1/2 fieldFilter on a level-0 field.
field_validation_failed, hook_validation_faileda validator or hook refused the dataFix the data; the message says which field.
rate_limit_exceededtoo many requestsWait for Retry-After seconds.
internala server faultRetry later; details are logged server-side.

30. FAQ

I put a user in a doc's readers — why can other members still read it?

A doc's own arrays control who can change or delete it. Reading is decided by the app, the resource and scope bindings. Keep private records in a resource whose readers exclude those members. Scope bindings also work, but only on servers where the operator has turned scope enforcement on (it is off by default).

I revoked someone's access with noaccess, but a share link still works.

Links are not blocked by noaccess. Revoke the share itself.

Why can't I filter by the email field?

It is encrypted (level 1 or 2), and encrypted fields cannot be searched. Keep a level-0 field for anything you need to filter on.

My session ended although I was active.

Sessions last 24 hours from sign-in, whatever you do in between. Sign in again.

Can I delete my own app?

The Owner can: the first delete is soft; deleting it again removes the app and its data permanently.

How do I export my data?

POST /user/data/export returns your account data; an app's docs and files are exported through /app/doc/list and /app/file/* (or the SDK).

31. Glossary

ACL — the arrays of tokens that decide who can do what.

AEAD — authenticated encryption; tafalo5 uses ChaCha20-Poly1305.

App — an independent workspace with its own members, data and front-end.

data_indexed / data_secret — where a doc's level-0 (plain) and level-1/2 (encrypted) fields are stored.

Doc — one record of a resource.

G_author — the token every signed-in user holds.

Hook — a declarative rule a resource runs around doc changes.

Level — a field's encryption level: 0 plain, 1–2 encrypted.

Permission set — the tokens a caller holds for one request.

Resource — a typed collection: fields, hooks and ACL.

Role — a named set of members in one app; its token is [r-<uuid>].

Scope binding — a rule that limits which rows a user sees by attribute; enforced only where the operator has turned scope enforcement on.

Share — a read-only link to one doc, carried by a token.

Site engine — draft → publish → rollback for an app's front-end.

Soft delete — hidden but kept (deleted_at set).

WASM operator — your own server-side code, run in a sandbox.