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
| Level | Comes from | Can |
|---|---|---|
| Owner | the app's creator (or whoever it was transferred to) | everything, including deleting the app, billing and unbinding domains |
| Manager | managers | app ACL (except managers, which only the Owner changes), roles, resources, site publishing, domains |
| Designer | designers | members and direct grants |
| Editor | editors | create and change data and files |
| Reader | readers | read |
3. Quick start — 5 steps
- Register an account and confirm your email (the link arrives by email).
- Sign in — the server sets a session cookie.
- Create an app — you become its Owner.
- Create a resource and your first docs.
- Publish a front-end and, optionally, bind your own domain.
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).
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-setand resource create/update add them to a rawr-<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:
- 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.
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
- Prerequisite: a confirmed email and a free app slot on your plan.
- Send
POST /app/updatewith{data: {name, description}}(notid= create). - The server returns the new
tid(a-<uuid>); you are its Owner.
| Refusal | Cause | What to do |
|---|---|---|
HTTP 402, code quota_exceeded | No app-creation right left | If 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_verified | Email not confirmed yet | Follow the confirmation link (it can be sent again). |
12. Workflow — Bind a custom domain
- Preview:
POST /app/domain/preview {app_tid, domain}returns the DNS records to create. - Configure DNS: an
Arecord pointing at the platform, or aTXTrecord at_tfl5.<domain>— either one proves ownership. - Add:
POST /app/domain/add {app_tid, domain}(Manager). If neither the A record nor the TXT proof is found, the call is refused anddatalists the records to create. On success,warningsflags a domain proven by TXT that does not point at the server yet. - TLS: the certificate is issued automatically on the first HTTPS request.
- 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
}
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}replacesdata: 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-setchanges a doc's own ACL (Owner/Manager, or the doc's author).
15. Workflow — Files & publishing
- Upload:
POST /app/file/uploadas multipart (app_tid, then pairs ofpath+file). Up to 50 MiB per file. - Stages: each file is in the
test(draft) orrelease(live) stage. Uploads default totest; reads default torelease. Sendstage=releaseto 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
noaccessentry does not block a link — revoke the share instead. - Turning
sharingoff 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
| Array | Grants |
|---|---|
managers | Manager 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. |
designers | Designer level (app) |
editors | create and change data |
readers | read |
deletable | delete: 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. |
noaccess | deny, 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
| Layer | Set with | Governs |
|---|---|---|
| App | /app/acl-set | everything in the app |
| Resource | /app/resource/create or /app/resource/update | reading and writing that resource's docs |
| Scope bindings | /app/scope/set | which 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-set | changing and deleting that doc |
| File | /app/file/acl-set | that 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?
| What | Who |
|---|---|
| App ACL | Manager (only the Owner can change managers) |
| Roles and their members | Manager |
| Direct grants to a member | Designer |
| Doc ACL | Owner/Manager, or the doc's author |
| File ACL | Manager |
| Groups | the 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.
| Limit | Applies to |
|---|---|
user_max_apps | apps you can own, on an account without app-creation rights |
user_max_total_storage | storage across all your apps |
app_max_storage | storage 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,SecureandSameSite=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/loginand/app/site/*, put their fields besideresult). 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 acode. Branch oncodeandisSignout, never onmsg, and do not rely on the HTTP status alone — some refusals answer HTTP 200. - Every data call names its app with
app_tidin the body. - Filtering is allowed on level-0 fields only.
26. Data lifecycle
| Entity | Delete |
|---|---|
| Doc | Soft delete (/app/doc/del). |
| Resource | Deleting it drops its docs (Manager); refused while another resource links to it. |
| File | To the trash; restorable until a Manager purges it or the trash retention (30 days by default) runs out. |
| App | Owner only. The first delete is soft; deleting again removes it and its data for good. |
| Account | Erased after a grace period; refused while you own apps. |
| Share | Revoked at once. |
| Domain | Marked 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.
| Type | Does |
|---|---|
require_fields | refuses the write unless the listed fields are present (code hook_validation_failed) |
set_fields | stamps values onto the doc after it is written |
webhook | POSTs 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) |
wasm | calls 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
| Form | Use |
|---|---|
<script src="/sdk.js"> | any page on your app's domain; defines window.TFL5 (ESM: /sdk.mjs) |
@tfl5/sdk | build 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
code | Cause | Action |
|---|---|---|
unauthorized | not signed in, or the session expired — the body is {isSignout: true, result: true}, so check isSignout as well as result | Sign in again. |
access_denied | signed in but not allowed | Ask a Manager to grant access. |
email_not_verified | email not confirmed | Follow the confirmation link. |
validation_email_taken | the email belongs to another account | Sign in to that account or use another address. |
quota_exceeded, quota_app_max_storage | a plan limit was reached | Free space or upgrade. |
cannot_filter_encrypted_field | filtering on a level-1/2 field | Filter on a level-0 field. |
field_validation_failed, hook_validation_failed | a validator or hook refused the data | Fix the data; the message says which field. |
rate_limit_exceeded | too many requests | Wait for Retry-After seconds. |
internal | a server fault | Retry 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.