Dynamic project API
Prefix: /api/p/:projectId/:databaseId (every database of a project has its own tables, users and files). JSON envelope: see REST conventions. Conceptual guide: End-user auth.
This is not platform auth (/api/auth/...). Platform register/login creates a console user on app.tefily.com. These routes create an app user in the project _users collection and return a project JWT (type: project).
Credentials: project JWT, platform JWT/PAT, or service token (kfxs_…). Platform and service tokens bypass access rules, except requireServiceToken which allows only a matching service token. Project JWTs are subject to rules.
Auth routes below are public (no Bearer token) except GET /auth/me (project JWT) and POST /auth/verify (service token). They require project auth to be enabled.
Enable project auth
Section titled “Enable project auth”Project auth is opt-in. Toggle it per database in Database, or POST /api/projects/:projectId/databases/:databaseId/auth/toggle with a platform JWT or PAT. Tefily creates _users (email, password, emailVerified) and an internal _auth_tokens collection for one-time email tokens.
_users endpoints stay disabled. Do not expose _users as a public CRUD collection. Use the auth routes.
Auth flow
Section titled “Auth flow”- The app calls register. Tefily creates
_userswithemailVerified: falseand sends a verification email. No JWT. - The user opens the link. Tefily’s hosted page calls verify-email, or the app posts the token itself.
- The app calls login. Tefily returns a project JWT (24 hours).
- The app sends
Authorization: Bearer <project jwt>on collection and file requests. - A server that receives that JWT should call verify with a service token. Do not trust the JWT locally.
Password reset is separate: request → email → complete → login with the new password.
register ──email──► click link ──► verify-email ──► login ──► project JWT │password-reset/request ──email──► complete ──► login ─────────┘Login is rejected with 401 and Email not verified until verify-email succeeds. Existing _users rows that have no emailVerified field are treated as verified so older data keeps working.
Emails
Section titled “Emails”Tefily sends the mail (Resend when configured). APP_NAME is the project name. USER_NAME is _users.name, then displayName, then the email local-part.
Links use the console origin (PUBLIC_APP_BASE_URL, hosted: https://app.tefily.com) and are hash routes. The pages are public (no console login). They call the same APIs as the SDKs.
| When | Link | Token TTL | |
|---|---|---|---|
| Verify address | Register; resend | {app}/#/auth/verify-email?projectId=…&token=… |
24 hours |
| Reset password | Reset request | {app}/#/auth/reset-password?projectId=…&token=… |
1 hour |
| Password changed | After a successful reset | none | - |
Tokens are random, stored hashed, and single-use. Issuing a new token of the same kind invalidates unused earlier ones. Reusing a spent or expired token returns 400 (Invalid or expired token).
resend-verification and password-reset/request always return { "sent": true } so callers cannot probe whether an email exists. Mail is only sent when there is a matching unverified user (resend) or any matching user (reset).
Register returns emailVerificationSent so you can tell whether the mailer accepted the message. It does not change the “always opaque” behavior of resend/reset.
You do not have to host verify or reset UI. If you want your own screens, read projectId and token from the link and call verify-email / password-reset/complete. You cannot change the URL Tefily puts in the email.
Auth endpoints are rate-limited more strictly than the rest of the API.
Two verify endpoints
Section titled “Two verify endpoints”| Path | Who calls it | Body | Meaning |
|---|---|---|---|
POST /auth/verify-email |
Browser / app (or the hosted page) | { "token": "<email token>" } |
Confirm the address. No JWT. |
POST /auth/verify |
Your backend with a service token | { "token": "<project JWT>" } |
Check a session. { valid, user? } |
Auth endpoints
Section titled “Auth endpoints”| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/register |
none | Create _users row; send verification email; no JWT |
| POST | /auth/login |
none | Login (verified email required); project JWT |
| GET | /auth/me |
project JWT | Current _users row |
| POST | /auth/verify |
service token | Body { "token" } → { valid, user? } |
| POST | /auth/resend-verification |
none | Body { "email" } → { sent: true } |
| POST | /auth/verify-email |
none | Body { "token" } → { verified, user } |
| POST | /auth/password-reset/request |
none | Body { "email" } → { sent: true } |
| POST | /auth/password-reset/complete |
none | Body { "token", "password" } → { reset: true } |
Content-Type must be application/json. Paths below are relative to https://api.tefily.com/api/p/{projectId}/{databaseId}.
Register
Section titled “Register”POST /api/p/{projectId}/{databaseId}/auth/registerContent-Type: application/json
{ "email": "jane@example.com", "password": "secret123", "name": "Jane"}{ "success": true, "data": { "user": { "_id": "…", "email": "jane@example.com", "name": "Jane", "emailVerified": false, "_createdAt": "…", "_updatedAt": "…" }, "emailVerificationSent": true }}emailandpasswordare required. Email is trimmed and stored lowercase.- Password must be 8–72 characters.
- Extra keys become
_usersfields (validated if you added them to the schema).passwordis never returned.emailVerifiedin the body is ignored and stored asfalse. - Duplicate email:
409(Unable to complete registration). - Auth off or unknown project:
404.
SDK: client.auth.register({ email, password, extraFields: { name: 'Jane' } }).
Verify email
Section titled “Verify email”The hosted page posts this when the user clicks the mail link. Your app can post it too.
POST /api/p/{projectId}/{databaseId}/auth/verify-emailContent-Type: application/json
{ "token": "<from VERIFY_URL>" }{ "success": true, "data": { "verified": true, "user": { "email": "jane@example.com", "emailVerified": true } } }Does not return a JWT. The user signs in with login next.
Resend verification
Section titled “Resend verification”POST /api/p/{projectId}/{databaseId}/auth/resend-verificationContent-Type: application/json
{ "email": "jane@example.com" }{ "success": true, "data": { "sent": true } }POST /api/p/{projectId}/{databaseId}/auth/loginContent-Type: application/json
{ "email": "jane@example.com", "password": "secret123" }{ "success": true, "data": { "token": "eyJ…", "user": { "_id": "…", "email": "jane@example.com", "emailVerified": true } }}The JWT payload includes sub (_users._id), type: "project", projectId, and jti. Expiry is 24 hours. The JS and Dart SDKs store it and send Authorization: Bearer on later calls.
| Status | Message |
|---|---|
| 401 | Invalid email or password |
| 401 | Email not verified |
Current user
Section titled “Current user”GET /api/p/{projectId}/{databaseId}/auth/meAuthorization: Bearer <project jwt>{ "success": true, "data": { "user": { "_id": "…", "email": "jane@example.com" } } }Requires a project JWT for this projectId. Platform, PAT, and service tokens are rejected.
Password reset
Section titled “Password reset”Request (always { sent: true }; mail only if the user exists):
POST /api/p/{projectId}/{databaseId}/auth/password-reset/requestContent-Type: application/json
{ "email": "jane@example.com" }The email links to {app}/#/auth/reset-password?projectId=…&token=…. The hosted page collects a new password and calls complete. Password must be at least 8 characters.
POST /api/p/{projectId}/{databaseId}/auth/password-reset/completeContent-Type: application/json
{ "token": "<from RESET_URL>", "password": "newsecret" }{ "success": true, "data": { "reset": true } }After a successful reset Tefily sends the password changed notification. Complete does not return a JWT. The previous password no longer works. Tokens cannot be reused.
SDK: requestPasswordReset(email) then completePasswordReset({ token, password }).
Verify a project JWT (server)
Section titled “Verify a project JWT (server)”Your backend must not treat a client-supplied JWT as valid without checking it.
POST /api/p/{projectId}/{databaseId}/auth/verifyAuthorization: Bearer kfxs_…Content-Type: application/json
{ "token": "<project jwt from the client>" }{ "success": true, "data": { "valid": true, "user": { "_id": "…", "email": "jane@example.com" } } }Invalid, expired, wrong-project, or unknown user: { "valid": false } (still HTTP 200). Missing service token: 403. See Service tokens and the backend guide.
Admins can mint a project-user JWT for the API tester (POST /api/projects/:projectId/users/:userId/token). Do not use that instead of login in production apps.
Send email
Section titled “Send email”Send a message from the project’s own address. Service token only: project JWTs, platform tokens, and anonymous callers get 403.
POST /api/p/{projectId}/email/sendAuthorization: Bearer kfxs_…Content-Type: application/json
{ "to": "jane@example.com", "subject": "Your order shipped", "html": "<p>It is on the way.</p>", "text": "It is on the way.", "replyTo": "help@example.com"}{ "success": true, "data": { "sent": true } }tois one valid address.subjectis one line, at most 200 characters. Sendhtml,text, or both.replyTois optional.- You cannot set
from. Mail is sent as"Project Name" <project-name-a1b2c3@tefily.com>, the address shown in Project Settings. The six-character suffix is fixed per project, and a rename changes only the name part. 400for an invalid body,403for the wrong credential,413for an oversized body.- Each send counts toward
max_transactional_emails_per_day(100 / 2,000 / 10,000 on Free / Pro / Scale). Over the cap returns the usual429limit error and nothing is sent. See Usage and limits.
Register, verification, and password-reset emails are sent from the same address, and count toward max_end_user_emails_per_day.
To send mail when data changes without writing code, use email triggers.
Documents
Section titled “Documents”| Method | Path | Description |
|---|---|---|
| GET | /:tableName |
List (page, pageSize, filter, sort, fields, expand) |
| GET | /:tableName/:id |
Get (fields, expand) |
| POST | /:tableName |
Create (JSON object) or bulk create (JSON array, max 100) |
| PUT | /:tableName/:id |
Update. PUT validation requires required fields in the body; PATCH does not. |
| PATCH | /:tableName |
Bulk update by filter (partial patch; omitted fields stay) |
| DELETE | /:tableName/:id |
Delete |
| DELETE | /:tableName |
Bulk delete by filter |
The table name is the Database schema name. Endpoints must be enabled. See Query. After auth is on, put requireAuth (and owner compares) on every collection method your app uses. See Access rules.
fields is a JSON array of field names (_id is always returned). expand is a JSON array of { "localField", "from"?, "as"?, "foreignField"?, "fields"?, "limit"? }. Omit from to expand a reference field that has a stored target collection. Explicit joins require from and as. At most 5 entries; nested related documents are capped at 50 by default (max 100). The joined collection’s list rules always apply.
| Method | Path | Description |
|---|---|---|
| POST | /:table/:recordId/files/:field/upload-sessions |
Start presigned upload (filename, contentType, size) |
| POST | /:table/:recordId/files/:field/:fileId/complete |
Finalize (uploadSessionId) |
| GET | /:table/:recordId/files/:field/:fileId |
Presigned download URL |
| DELETE | /:table/:recordId/files/:field/:fileId |
Delete file |
Full URL example:
https://api.tefily.com/api/p/PROJECT_ID/DATABASE_ID/posts