Example: order manager
A small project to see NotusBase end to end: login, several companies in the same database (tenantId), an attached file, a scheduled task, a webhook route and a cron. It is the guide's example; the rules it follows are in 03-data-modeling.md. The test starts a new server, deploys the project and checks everything with the official convex client.
Running the test
It needs the notusbase binary (how to build it: 01-quick-start.md, section 0) and Node 22 or newer.
npm install
NOTUSBASE=/path/to/notusbase npm test
The test starts the server through the test kit, on a free port (PORT=... fixes one) and in tmp/kit/default/ inside this folder (NOTUSBASE_TEST_DIR=... changes it), deleting the folder before it starts. It ends with ok, in about 2 s. The business errors the test triggers on purpose are not printed: the kit's client has no logger.
What is in it
| Module | What it shows |
|---|---|
convex/schema.ts | companies, members, orders, events. Every company data index starts with tenantId. v.id("_storage") and v.id("_scheduled_functions") as references |
convex/access.ts | currentMember(ctx): identity → row in members → tenantId. The tenantId never comes from the client. orderOfCompany denies another company's order |
convex/companies.ts | create (creates the company and the first owner member), mine |
convex/orders.ts | create, list, summary (sum by status through the index), changeStatus (state machine), history. create accepts expiresInMs, which schedules expire; changeStatus cancels that job |
convex/files.ts | Receipt: uploadUrl → the client POSTs the file → attachReceipt (checks the size, deletes the previous one) → receiptUrl |
convex/http.ts | POST /webhook/payment, protected by x-secret against WEBHOOK_SECRET (a server variable) |
convex/crons.ts, convex/maintenance.ts | Daily cleanup of events older than 90 days (chained batches of 1,000) and of files uploaded and never attached for more than a day |
test.mjs | The end-to-end test |
How login works
The server starts with NOTUSBASE_AUTH_COLLECTIONS=users, so the PocketBase token counts as the identity (subject = id of the record in users). The kit creates each user through the PocketBase API (/api/collections/users/records, with a test superuser), signs in with auth-with-password and gives the token to the client (client.setAuth(...)): in the test, t.addUser(email) returns the ready client. Each user creates their company with companies:create.
Open signup: with NOTUSBASE_SIGNUP=1, anyone can sign up. Here that is harmless because whoever has no row in members gets NO_COMPANY and reads nothing. In a closed product, leave the variable out.
What the test checks
- A call without login →
NOT_AUTHENTICATED; login without a company →NO_COMPANY. - Two companies: each lists only its own orders, and reading or changing the other's order gives
ORDER_NOT_FOUND. - States
open → paid → shipped(orcancelled),INVALID_TRANSITIONwhen going back,INVALID_AMOUNTfor a negative amount, and the realtime subscription seeing the new order. - Summary by status and event history.
- File upload through the signed link, attach and read through the URL. Another company that knows the
storageIdcan neither attach nor delete it (usedFilestable), and the 6 MiB upload is refused by the server (NOTUSBASE_UPLOAD_MB=5in the test). - An order with a 300 ms expiry becomes
cancelledon its own; an order paid before it expires cancels the job. - Webhook: 401 with the wrong secret; with the right one, the order becomes paid (
{changed: true}), and repeating the call changes nothing ({changed: false}): it is idempotent. notusbase run maintenance:cleanOldEvents.
Before copying into a product
- Whoever signs up can ask for upload links.
uploadUrlonly requires being a member of a company, andcompanies:createis open to any user. In a real product, add a quota (per company or per day) and restrictcompanies:create. - The upload size cap belongs to the server (
NOTUSBASE_UPLOAD_MB, 1,024 by default), not to the function. The example starts the test server with 5; in production, adjust the variable and the proxy's limit. - Stray files (uploaded, never attached) stay on disk until the daily cron deletes them.
Not covered
- Interface: only the Node client.
- Load, reconnection and server crashes.
- External authentication provider (
auth.config.ts). - The cron is not waited for (the test calls the function through
notusbase run).
To use as a starting point
Copy the folder, swap the domain modules (orders, files) and keep access.ts, the members design and tenantId in the indexes. In development: notusbase dev. To deploy to a running server: npm run deploy -- --url <server>.