Skip to content

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.

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.

  1. The app calls register. Tefily creates _users with emailVerified: false and sends a verification email. No JWT.
  2. The user opens the link. Tefily’s hosted page calls verify-email, or the app posts the token itself.
  3. The app calls login. Tefily returns a project JWT (24 hours).
  4. The app sends Authorization: Bearer <project jwt> on collection and file requests.
  5. 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.

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.

Email 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.

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? }
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}.

POST /api/p/{projectId}/{databaseId}/auth/register
Content-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
}
}
  • email and password are required. Email is trimmed and stored lowercase.
  • Password must be 8–72 characters.
  • Extra keys become _users fields (validated if you added them to the schema). password is never returned. emailVerified in the body is ignored and stored as false.
  • Duplicate email: 409 (Unable to complete registration).
  • Auth off or unknown project: 404.

SDK: client.auth.register({ email, password, extraFields: { name: 'Jane' } }).

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-email
Content-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.

POST /api/p/{projectId}/{databaseId}/auth/resend-verification
Content-Type: application/json
{ "email": "jane@example.com" }
{ "success": true, "data": { "sent": true } }
POST /api/p/{projectId}/{databaseId}/auth/login
Content-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
GET /api/p/{projectId}/{databaseId}/auth/me
Authorization: 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.

Request (always { sent: true }; mail only if the user exists):

POST /api/p/{projectId}/{databaseId}/auth/password-reset/request
Content-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/complete
Content-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 }).

Your backend must not treat a client-supplied JWT as valid without checking it.

POST /api/p/{projectId}/{databaseId}/auth/verify
Authorization: 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 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/send
Authorization: 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 } }
  • to is one valid address. subject is one line, at most 200 characters. Send html, text, or both. replyTo is 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.
  • 400 for an invalid body, 403 for the wrong credential, 413 for 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 usual 429 limit 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.

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