End-user guide for tafalo5
tafalo5 (codename tfl5) is a multi-app, schema-driven data platform: you declare your data shape (resource), create records (docs), bind your own domain, and share with fine-grained control. This guide is for app owners, data designers, editors, regular users, and anonymous visitors.
2. Model & roles
Everything in tafalo5 revolves around four objects and a single permission check. Understanding the diagram below covers ~90% of platform usage.
platform (system-wide, singleton)
├── user (account; shared across all apps)
├── group (global group, peer of app)
└── app (your workspace — literally called "app")
├── role (in-app role)
├── resource (data type / schema)
│ └── doc (a concrete record)
└── folder/file (binary store)
2.1 Roles you may encounter
| Code | Role | Meaning |
|---|---|---|
Anon | Anonymous visitor | Not signed in. Reads only what's public. |
User | User | Signed in; automatically a member of G_author. |
AppMgr | App manager | Listed in app.managers. Can change the app's own ACL. |
AppDes | App designer | Creates & edits resources and roles. |
AppDev | App developer | Creates root files/folders inside the app. |
ResMgr / ResDes | Resource manager / designer | Edits the ACL or schema of a specific resource. |
Author | Doc author | The user who created a doc. Immutable after creation. |
Editor / Reader | Editor / Reader | Listed in doc.editors or doc.readers. |
GrpMgr | Group manager | Edits a group's members or ACL. |
Operator | Platform operator | Has command-line access to the platform infrastructure. Not a runtime-API role. |
AppMgr on app A and only a Reader on app B at the same time.
3. Quick start — 5 steps
- Register an account (username, email, password ≥ 6 chars).
- Sign in — the server issues a 24-hour session cookie.
- Create an app — you become its sole
app.managersentry. - Bind a domain (optional) — add A + TXT records to your DNS.
- Create your first resource and doc — your data is online.
https://<domain>/cpanel or via the JSON API (see §28 SDK).
4. App
An app is an independent workspace with its own schema, data, users, and any number of bound domains.
- Each app has its own per-app encryption key — sensitive fields are encrypted with it.
- The creator becomes
app.authorand the only entry inapp.managers. - Your maximum number of apps is bounded by
license.user_max_apps(see §23). - Ownership can be transferred to another user.
5. Resource
A resource is a schema — you declare fields, validators, and per-field sensitivity (public / private / secret). When a user creates a doc, the platform:
- Validates
dataagainst each field's rules; - Runs the
before_createDSL/hooks; - Generates
doc.tidand stampsdoc.author; - "Freezes" the doc's ACL from the
resource.aclstemplate; - Encrypts every field whose
sensitivity != "public"with the app_key.
6. Doc
A doc is one concrete record of a resource. After creation its ACL is immutable — nobody can change it directly. Only app.managers can re-apply the template in bulk via POST /data/reset/<resource_id>.
| Field | Meaning |
|---|---|
author | UUID of the creator, immutable. |
editors | Who may edit doc.data. |
readers | Who may read. [] = public. |
deletable | Who may delete the doc. |
noaccess | Absolute deny list. |
histories | Append-only: [ts, user, action]. |
7. Files & folders
Files and folders are each app's binary store. A folder can carry a folder.acls template that is stamped onto child files/subfolders at creation time. Each file is stored under <app_tid>/files/<tid>/<tid>.bin in the platform's binary store; the original filename is encrypted before it's saved.
8. Domain
A domain is bound to an app after DNS verification succeeds. An app can host many domains, and each can have its own theme, language, noaccess list, and URL routes.
9. Groups & roles
A group is global (peer of an app). A role exists inside one app. Both can appear in any ACL list.
G_author= every signed-in user (implicit).anonymous= every visitor (implicit).G_<uuid>= a global group you created.[<uuid>]= an in-app role — note the square brackets.
10. Workflow — Register & sign in
10.1 Register
Endpoint: POST /reg with body {username, password, re_password, email}.
- Username must be unique; email must be unique.
- Password must be ≥ 6 characters and match
re_password. - The email is encrypted with the master key before storage; only a hash is kept for lookup.
- Per-IP rate limiting via the
TFL5_RATE_LIMIT_REGenv var.
10.2 Sign in
Endpoint: POST /login with {username, password}. The server issues an _token cookie:
- Value is authenticated-encryption ciphertext — not readable client-side.
- Lifetime 24h, with rolling refresh on every authenticated request.
10.3 Change password
Self-reset: POST /user/resetp {old_password, new_password}. Wrong old password → 400. After the change, existing sessions can be invalidated if the operator has enabled token-version bumping.
10.4 Sign out
POST /logout — the server sets the cookie to empty with Max-Age=0. Then POST /user returns {isSignout: true}.
10.5 Delete account
Self-delete succeeds only when user.app_count == 0. If you still own apps, you must delete or transfer them first.
11. Workflow — Create an app
- Prerequisite: Your account is in
platform.designers(default isG_author, so every signed-in user can create apps). - Send
POST /app/updatewith{data: {name, description, ...}}. - The server generates
app.tid, setsapp.author = you, adds you toapp.managers. - A per-app encryption key is minted for the new app.
user.app_countis incremented by 1.
| Error | Cause | How to resolve |
|---|---|---|
| 400 "Quota exceeded" | Exceeded user_max_apps | Delete an old app or upgrade your license. |
| 403 | Listed in platform.noaccess | Contact your operator. |
12. Workflow — Bind a custom domain
Convention: verify-then-save. A domain row is only persisted with active=true after DNS proof passes.
- Preview:
POST /app/domain/preview {app_tid, domain}— the server returns the A-record target and a verify token. - Configure DNS:
A:<domain>→platform.domain_a_record_target.TXT:_tfl5.<domain>→ theverify_tokenvalue.
- Verify & save:
POST /app/domain/add {app_tid, domain, verify_token}. Both DNS records correct → adomainsrow is inserted withactive=true. - TLS: Auto-TLS mints the certificate on the first request to that domain.
- Weekly re-verify: The platform re-checks DNS periodically. Four consecutive failures (~30 days) flip
active=falseand notify you.
Per-domain configuration (theme, language, noaccess, routing)
POST /app/domain/update/<domain_tid> {theme, lang, single_page, url_routes, noaccess} lets one app behave differently per domain. For example, intranet.acme.com can set noaccess: ["anonymous"] to block anonymous visitors while acme.com stays publicly readable.
13. Workflow — Design a resource
Only AppDes (members of app.designers) can create resources.
POST /app/resource/add
{
"data": {
"ma": "post", // unique code within the app
"name": "Blog post",
"fields": [
{ "field": "title", "type": "text", "sensitivity": "public", "validator": ["required","minlen:3"] },
{ "field": "body", "type": "html", "sensitivity": "public" },
{ "field": "secret", "type": "text", "sensitivity": "private" }
],
"acls": {
"editors": ["@author"],
"readers": [], // [] = public
"deletable": ["@author"]
},
"sharing": true
}
}
public fields live plaintext in data_indexed and are filterable. private/secret fields are encrypted in data_secret — not filterable; filtering on them returns 400.
14. Workflow — Doc CRUD
14.1 Create a doc
POST /data/add/<resource_id>
{ "data": { "title": "Hello world", "body": "..." } }
14.2 List & filter
POST /data/gets/<resource_id>
{
"filter_rules": [
{ "field": "title", "op": "contains", "value": "hello" }
],
"skip": 0,
"limit": 20 // capped at 100
}
The query layer automatically folds ACL conditions into the SQL — you only receive docs you're allowed to read. Docs reached through a share are field-projected by share.fields.
14.3 Update & delete
POST /data/edit/<resource_id>/<doc_id>— partial merge ofdata, appends tohistories.POST /data/del/<resource_id>/<doc_id>— soft delete (setsdeleted_at).
14.4 Bulk ACL reset
AppMgr only: POST /data/reset/<resource_id> {filter_rules}. The server recomputes editors/readers/deletable/noaccess for each matching doc from the current resource.acls template, preserving author.
15. Workflow — Upload & download files
- Init:
POST /app/file/upload-init {filename, size}→ returns{file_tid, upload_url}(presigned PUT, 10-minute TTL). - PUT to S3 directly from the client (bypasses the server).
- Finalize:
POST /app/file/finalize {file_tid}— the server HEADs the object, verifies size, writes the row, charges quota. - Download:
GET /file/<file_tid>→ 302 redirect to a signed S3 URL (5-minute TTL).
| Situation | Behavior |
|---|---|
Exceeds app_max_storage or user_max_total_storage | 400 "Storage limit reached". |
| Declared size ≠ S3 object size | Reject and delete the S3 object. |
License on_quota_exceed = read_only | Blocks every POST CRUD (advanced). |
16. Workflow — Sharing
Sharing is read-only at the doc level. You cannot share folders, files, or whole resources. Editing and deletion still go through the formal ACL.
16.1 Mint for a specific user
POST /share
{ "app_id": "...", "resource_id": "post", "doc_id": "...",
"target": "u_bob", "fields": null }
16.2 Mint a public link
POST /share
{ ..., "target": "anonymous", "generate_token": true,
"expires_at": 1893456000000 }
=> { "share": {...}, "token": "<plaintext_one_time>",
"url": "https://acme.com/?_share=<plaintext>" }
16.3 Field filter
fields: ["title","summary"] → the recipient only sees those two fields. Other fields are stripped; metadata (tid, author, created_at...) is always shown. ACL fields are always hidden from share-based access.
16.4 Revoke
DELETE /share/<share_tid> — sets revoked_at. The row is kept for audit. Automatic cleanup after 12 months.
16.5 Interaction with other rules
- noaccess beats share. An app with
noaccess: ["anonymous"]denies even a valid public token. resource.sharing = falseis a kill switch: new shares get 400; existing shares are temporarily ignored (and reactivated if the flag is flipped back).- Soft-deleted doc → all of its shares stop working (every check injects
deleted_at = 0).
17. Workflow — Multiple domains, one app
- Bind both
acme.comandacme.vnto the same app — shared data, different theme/language. intranet.acme.comwithnoaccess: ["anonymous"]— public is locked out.- Per-domain
url_routes:"/posts/:slug"→ server-side render with the doc preloaded.
18. Access control — the permission set
On every request the server builds a permission set:
permission_set = [
user.tid, // bare uuid
...user.groups.map(g => g.tid), // "G_<uuid>"
...user.roles_in_current_app.map(r => "[" + r.tid + "]"),
is_authenticated ? "G_author" : "anonymous"
]
A request "passes" a gate when permission_set ∩ gate_field ≠ ∅ — i.e., they share at least one element.
19. ACL fields
| Field | Gates this op | Notes |
|---|---|---|
managers | Edit the entity's ACL (and license_tid on apps). | Absent on docs (docs are immutable). |
designers | Create/edit child schemas (resources, roles). | On platform: also "create app + create group". |
developers | Create root files/folders inside an app. | App tier only. |
authors | Create docs of a resource. | Resource tier only. |
editors | Edit content / metadata. | |
readers | Read content. readers: [] means public. | Important. |
deletable | Delete the entity (also requires noaccess clearance). | |
noaccess | Absolute deny, cascading to children. | Always wins over any allow. |
20. noaccess cascade
platform.noaccess → denies: app, group, user (everything below)
app.noaccess → denies: resource, doc, file, folder, role
resource.noaccess → denies: docs of that resource
folder.noaccess → denies: file + subfolder inside (recursive)
doc/file/role/group/license.noaccess → leaf (the entity itself only)
deny_set when checking entity E:
deny_set = platform.noaccess
∪ A.noaccess (E belongs to app A)
∪ R.noaccess (E belongs to resource R)
∪ F.ancestors.noaccess (E is a file/folder, recursive)
∪ E.noaccess
permission_set ∩ deny_set ≠ ∅ → DENY immediately, no further checks.
21. Templates & @author
Three places carry templates: resource.acls, folder.acls, app.acls. When a child entity is created, the template is applied overwrite-style (not union) to the child's ACL fields, and the pseudo-token @author is replaced with the creator's UUID.
resource.acls.editors = ["@author", "[t_editor_role]"]
→ alice (u_alice) creates doc d1:
d1.author = "u_alice"
d1.editors = ["u_alice", "[t_editor_role]"]
During reset, the server re-substitutes @author using the stored doc.author — the author never changes.
22. Who can change ACLs?
| Action | Authorized parties |
|---|---|
Edit platform.ACL | platform.managers (empty by default → CLI operator only). |
Edit license | license.managers. |
Edit app.ACL (incl. app.acls, license_tid) | app.managers. |
Edit resource.ACL (incl. acls, sharing) | resource.managers. |
Edit folder/file/group/role.ACL | The matching X.managers. |
Edit doc.ACL directly | Nobody — immutable. |
Bulk-reset doc.ACL | app.managers only, via POST /data/reset. |
23. Platform rules — Quotas & licenses
A license is a "plan" attached to a user or to an app and defines hard ceilings.
23.1 Quota fields
| Field | Applies to | Meaning |
|---|---|---|
user_max_apps | User | Maximum apps the user can own. |
user_max_total_storage | User | Total file storage across all of the user's apps. |
app_max_storage | App | Maximum storage for a single app. |
features | Both | custom_domain, anonymous_share, ... |
on_quota_exceed | License | block_upload (default) · read_only · suspend_domain. |
23.2 Enforcement algorithm
can_create_app(user):
permission_set ∩ platform.designers ≠ ∅
AND user.app_count < (user.override_max_apps ?? license.user_max_apps)
can_upload_file(user, app, size):
not (user in app.noaccess cascade)
AND permission_set ∩ app.developers ≠ ∅
AND user.used_storage + size ≤ (override ?? license.user_max_total_storage)
AND app.used_storage + size ≤ app_license.app_max_storage
23.3 Over-quota behaviors
- block_upload — new uploads are rejected; reads still work.
- read_only — every POST CRUD is rejected.
- suspend_domain — the domain returns 503 (v2).
23.4 Upgrades & overrides
Operator-only (CLI). Examples:
tfl5 user license <user_tid> pro
tfl5 user override <tid> --max-apps 100
24. Platform rules — Security & encryption
24.1 Encryption at rest
- Doc fields marked
private/secret→ authenticated-encryption ciphertext stored indata_secret. - User email → encrypted with the master key; only a hash is kept for lookup.
- Original filenames → encrypted into
files.original_filename_encrypted; the binary store sees only UUIDs. - ACL fields store UUIDs, not usernames or emails — a backup file does not leak PII.
24.2 Session cookie
- Authenticated-encryption ciphertext keyed by the platform passphrase (server-side).
- HttpOnly Secure SameSite=Lax
- Random nonce per encrypt → two consecutive logins produce two different cookies.
- 24-hour rolling lifetime; optional IP-binding (config) to invalidate cookies stolen from another IP.
24.3 Cross-tenant isolation
Each app has its own per-app encryption key, bound to app_tid. An operator with app A's key trying to decrypt an app B doc → cross-app check fails → decryption error.
24.4 Key rotation
tfl5 keys rotate-app <app_tid>
→ a new key version is minted
→ background job re-encrypts each doc
→ 30-day grace period before v1 is destroyed
24.5 Audit log tamper detection
Every audit row is hash-chained to the previous one. tfl5 audit verify-chain --last 30d spots tampering by detecting a broken hash.
25. Platform rules — Usage rules
- TLS required. Plain HTTP is redirected to HTTPS.
- Rate limiting on
/regand/share/claim, per IP. - Consistent response format: success
{result:true, ..., timestamp}; error{result:false, msg:<string|array>}. - No filtering on sensitive fields. Filter rules must target fields with
sensitivity: "public"; violations return 400. - No mock data in dev. Every test goes against real data so that real constraints are exercised.
- UI strings. The platform default is English. Tenants can override via the
/cpanel/langbundle.
26. Platform rules — Data lifecycle
| Entity | Soft delete | Hard delete / cleanup |
|---|---|---|
| Doc | deleted_at = now | Cleaned up when its resource is hard-deleted. |
| Resource | deleted_at = now | Docs of that resource are locked for CRUD immediately. |
| App | — | Cascade-deletes resources/docs/files/roles; the per-app encryption key has a 30-day grace period. |
| User | Blocked while they still own apps. | app_count = 0 → ban=-1 or hard delete. |
| Share | revoked_at = now | Weekly cron deletes/archives rows older than 12 months. |
| Domain | active = false | After 4 consecutive DNS verify failures (~30 days). |
27. Hooks & validators
Designers wire automated behavior into each resource:
27.1 Field validators
fields: [
{ "field": "title", "validator": ["required", "minlen:3"] }
]
27.2 DSL rules
before_create.set: {
"data.slug": "lower(replace(data.title, ' ', '-'))"
}
before_delete: [
{ "when": "doc.data.status == 'published'",
"deny": "Cannot delete a published post" }
]
27.3 Webhooks
after_create.call_webhook: "notify_slack"
before_create.call_webhook: { name: "validate_invoice", on_fail: "abort" }
- Hooks are pushed onto an internal queue; a worker delivers each one with an HMAC signature.
on_fail: "abort"+ the webhook returning{result:false}aborts the create.- Up to 3 exponential-backoff retries.
28. SDK & integration
Three distribution paths:
| Form | Use case | Output |
|---|---|---|
GET /sdk.js | SPA tenants — one-line script include | UMD bundle, registers window.TFL5. |
npm @tfl5/sdk | Build pipelines (Vite/Webpack/Next) | ESM + CJS. |
npm @tfl5/cli | Codegen TypeScript types from resource schemas | CLI binary. |
Browser
<script src="/sdk.js"></script>
<script>
const tfl5 = new TFL5();
await tfl5.login(username, password);
const posts = await tfl5.resource("post").list({ filter_rules: [], limit: 20 });
</script>
Node (server-to-server)
import { TFL5 } from "@tfl5/sdk";
const tfl5 = new TFL5({ host: "https://acme.com", appId: "a_xxx",
token: process.env.TFL5_TOKEN });
await tfl5.user();
Anonymous share claim
const tfl5 = new TFL5();
await tfl5.claimFromUrl(); // reads ?_share=... + calls /share/claim
const doc = await tfl5.resource("post").get(docId); // auto-attaches X-Share-Token
29. Common errors
| Message | Cause | Action |
|---|---|---|
| Invalid account or password | Wrong username, wrong password, or banned user | Double-check; contact your operator if it persists. |
| Email already in use | Email hash collision | Use a different email or sign in to the existing account. |
| Quota exceeded | App slot or storage limit hit | Free space or upgrade the license. |
| Access denied / Banned | Hit by a noaccess cascade or missing the required role | Ask an AppMgr to grant access or remove the noaccess entry. |
| Sharing disabled | resource.sharing = false | Ask a ResMgr to re-enable it. |
| Invalid config this domain | Domain not verified, or active = false | Re-run the bind-domain workflow. |
| Field not filterable | Filtering on a non-public field | Switch to a public field or drop the filter. |
| Storage limit reached | Exceeded app_max_storage or user_max_total_storage | Delete old files or upgrade. |
| DNS A record mismatch / TXT verification failed | DNS hasn't propagated or the value is wrong | Re-check DNS, wait for TTL, retry preview + add. |
| encryption unavailable | The encryption service is unavailable | Wait for the operator to recover — don't hammer retries. |
30. FAQ
I just created a doc, but another user still can't read it even after I added them to doc.readers — why?
A doc's ACL is frozen from resource.acls at creation time. Editing resource.acls afterwards doesn't retroactively change existing docs, and editing doc.readers directly is not allowed. The right move: ask an AppMgr to run POST /data/reset/<resource_id> with a filter so the new template is re-applied to existing docs.
I shared a public doc, but anonymous users still get denied.
Most likely the app or domain has noaccess: ["anonymous"]. noaccess always wins over share. Either remove anonymous from the cascading noaccess list, or change strategy and share with signed-in users only.
Why can't I filter by the email field?
Sensitive fields (private/secret) live encrypted in data_secret and have no index → no filtering. The designer would need to mark the field sensitivity: "public" and migrate (re-encrypt) existing data via the CLI.
My session cookie suddenly expired even though I'm active.
Rolling refresh only happens when you send a request with the cookie. If a SPA stays in cached views for > 24h without hitting the API, the cookie expires. Reload the page → sign in again.
Can I delete my own app?
Yes, if you're in app.managers ∩ app.deletable and the app isn't a reserved "native" one. After deletion the resources/docs/files/shares cascade out, and the per-app encryption key enters a 30-day grace period before final destruction.
Does a public share token persist after I close the tab?
The server stores only a hash. The plaintext is returned exactly once when minted. Lost it? Mint a new token and revoke the old one.
What's the rule on backups and data export?
Backups are operator-managed at the server layer. Backup files don't self-decrypt — access to the encryption service is required to restore them. Self-service export from inside an app: use POST /data/gets + GET /file/<tid> via the SDK. Bulk export through the CLI is a v2 feature.
31. Glossary
ACL — Access Control List. A token list that decides who can do what to an entity.
App — An independent workspace with its own schema, data, and users.
App key — Per-app encryption key. Used to encrypt sensitive fields and the original filenames of files belonging to that app.
app_tid / doc.tid — The unique identifier (UUID) of the corresponding entity.
Auto-TLS — On-demand TLS certificate minting: a certificate is created the first time a bound custom domain is hit.
Cascade — A parent-tier noaccess propagating down to every child entity.
Cell — A deployment unit (its own database + storage). v1 ships with the single cell default.
data_indexed / data_secret — Twin columns holding plaintext public fields (JSON) and encrypted sensitive fields.
Doc — A concrete record of one resource. ACL is immutable after creation.
DSL hook — A declarative rule (set/when/deny) that runs around doc CRUD.
G_author — The implicit group containing every signed-in user.
License — A plan attached to a user or app that determines quotas and features.
Permission set — The list of tokens a caller carries within one request.
Platform — The system-wide singleton config row.
Resource — Schema + ACL template + hooks for one kind of doc.
Role — A per-app role (its token is wrapped in square brackets).
Sensitivity — A field's classification: public / private / secret.
Share — A record granting read access to one doc for one target (user / group / role / anonymous).
Soft delete — Setting deleted_at > 0; the default filter hides such entities.
Token (ACL) — One identifier string: <uuid>, G_<uuid>, [<uuid>], G_author, or anonymous.
verify-then-save — The domain-binding principle: insert a row only after DNS proof passes.