NotusBase

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

ModuleWhat it shows
convex/schema.tscompanies, members, orders, events. Every company data index starts with tenantId. v.id("_storage") and v.id("_scheduled_functions") as references
convex/access.tscurrentMember(ctx): identity → row in members → tenantId. The tenantId never comes from the client. orderOfCompany denies another company's order
convex/companies.tscreate (creates the company and the first owner member), mine
convex/orders.tscreate, list, summary (sum by status through the index), changeStatus (state machine), history. create accepts expiresInMs, which schedules expire; changeStatus cancels that job
convex/files.tsReceipt: uploadUrl → the client POSTs the file → attachReceipt (checks the size, deletes the previous one) → receiptUrl
convex/http.tsPOST /webhook/payment, protected by x-secret against WEBHOOK_SECRET (a server variable)
convex/crons.ts, convex/maintenance.tsDaily 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.mjsThe 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

  1. A call without login → NOT_AUTHENTICATED; login without a company → NO_COMPANY.
  2. Two companies: each lists only its own orders, and reading or changing the other's order gives ORDER_NOT_FOUND.
  3. States open → paid → shipped (or cancelled), INVALID_TRANSITION when going back, INVALID_AMOUNT for a negative amount, and the realtime subscription seeing the new order.
  4. Summary by status and event history.
  5. File upload through the signed link, attach and read through the URL. Another company that knows the storageId can neither attach nor delete it (usedFiles table), and the 6 MiB upload is refused by the server (NOTUSBASE_UPLOAD_MB=5 in the test).
  6. An order with a 300 ms expiry becomes cancelled on its own; an order paid before it expires cancels the job.
  7. 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.
  8. notusbase run maintenance:cleanOldEvents.

Before copying into a product

Not covered

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