Docs › Reference
Building on Notato
Extend the server and board with your own storage, accounts and screens.
Building on Notato#
The server is a library as well as a command. runServe takes a ServerExtension, which is how a hosted product (Notato Cloud is one) is built on the same server instead of a copy of it: annotations, MCP, relay, webhooks and the event streams are the server's own, and the extension brings what differs.
import { runServe, type ServerExtension } from "@notato/server";
const extension: ServerExtension = {
name: "my-deployment",
store: () => new MyPostgresStore(url), // notes, projects and accounts
blobs: () => new MyObjectStore(bucket), // screenshots
hooks: ({ store }) => ({ beforeIngest: (note, bytes) => checkQuota(store, note, bytes) }), // throw a RequestError to refuse
auth: ({ store }) => new MyAccounts(store), // who is signed in: admins, members, tokens
http: ({ backend, auth }) => ({
routes: { private: [extraRoutes], public: [callbackRoute], isPublic: (_m, p) => p === "/callback" },
around: (request, next) => audit(request, next), // wraps every request once its credential is known
}),
mcp: () => ({ requireToken: true, maxSessions: 128 }),
start: (runtime) => startWorkers(runtime), // returns how to stop them
};
await runServe({ version, extension });
The extension's parts are made in stages. Later stages receive the parts already made:
| Part | What it supplies |
|---|---|
store, blobs | Records and accounts through Store & AuthStore, and screenshots through BlobStore; the defaults are SQLite and files |
config | A ConfigSource wrapping the settings file |
hooks | Checks before backend changes, such as refusing a note or project that would exceed a limit |
backend | An optional LocalBackend subclass, made with the assembled store, blobs, config and hooks |
auth | An AuthProvider; replacing it also takes over account creation from the built-in admin login |
http | Extra routes and rules for the built-in ones, authorization, middleware, allowed origins, headers, CSP and upload limits |
mcp | Session limits, a guard, a token requirement, tool auditing, and how a locked credential is refused |
fetch | A wrapper around every request, including MCP, before the HTTP handlers |
start | Background work after the server starts, returning an optional stop function run before the store closes |
- Accounts. A credential is an
admin, atoken, or amember: a signed-in person who reaches only theprojectIdsyour accounts give them. The core's routes, event streams and MCP sessions enforce that scope and end streams and sessions when the credential stops being valid. Extra routes must apply their own authorization checks. Setshare: falseto omit signed screenshot links from webhooks when screenshots must stay behind authentication. - The built-in routes, by your rules. Instead of replacing a built-in route,
httpcan return rules for it.projectRulessays who may create, rename and delete projects (assertManager), whatGET /projectslists for a credential (list; only projects the credential reaches are ever shown), and what a new project is made with (create: its id, extra fields for its record, and how its first tokens are issued).tokenRulesopens/tokensto more than the admin (assertManager), says which tokens each credential sees and may revoke (visible; by default only an admin sees any), and issues and shows them (issue,shown);/admin/tokensstays the admin's.aboutCalleradds fields to/auth/me. Frommcp,lockedRefusalis the status and message alockedcredential gets (403 by default). WithstrictOrigins, sign-in and sign-out are taken from the board only. - Extending the built-in accounts.
Authenticatorcan be subclassed rather than replaced. It keeps reading, rate-limiting, minting and storing credentials, and its protected hooks decide the rest:tokenPrincipalandsessionPrincipalsay who a valid token or session is (null refuses it),passwordRefusalturns a password sign-in away before the password is checked,newSessionsays what a session holds and how long it lasts, andendSessionandaddAdminrun as a session ends and the admin account is created.startSession,mintToken,cookieandsessionIdare there for a sign-in of your own. ThecookieNameoption renames the session cookie (a__Host-name is alwaysSecure), andqueryTokens: falserefuses tokens in?token=. - Typed parts and helpers.
ServerExtension<S, B, A>names the store, backend and accounts your extension makes: every later part receives them as those types, and so does whatrunServereturns. For a store or routes of your own,annotationColumnsgives the values the SQLite store indexes for each note,shownTokena token record without its hash, andcheckBodychecks a body the wayreadJsondoes. A command built onnotatocan importmain,parsePort,localServerandstopOnSignalsfrom@notato/cli. - Private asset reads. A custom store must honour
projectsWithAsset(id, { peopleOnly: false }): only notes shared with agents may grant an agent credential access to those image bytes. Returning the unfiltered projects could expose an image attached only to a People only note. - The board's screens.
mountBoard({ extension })takes aBoardExtension:Loginreplaces sign-in,Framewraps or gates the signed-in board,pagesadds routes at#/x/<page>, andnavlinks to them.SidebarTopadds a slot above the project's navigation,ProjectNavcan replace that navigation, andProjectSettingsExtraadds a section to project settings.signOutends the extension's session. - The board's account behavior.
readState.markReadpersists read markers for the signed-in account;accountScopeseparates its browser read markers by an additional scope;accountNamesupplies its name.automaticProjectIdsomits the id field when the server generates ids, andrequireTokenExpiryremoves the no-expiry choice.canManagecontrols project and token management, whilecanAdminServercontrols the server's own settings independently.canManageTokenscan permit token management separately;tokenssupplies creation choices, extra request options, scope labels, and whether agents or an all-projects choice are offered. The server must enforce those policies. An authentication extension may set a token principal'sauthorNameto bind app feedback to an account; captured and supplied reply names cannot override it. - From a checkout.
@notato/serverand@notato/boardare not published to npm: use this repository as a git submodule or a workspace. Build a board from your own page withbun packages/board/scripts/build.ts --html your.html, and serve it withNOTATO_WEB_DIST. A server with no extension keeps the SQLite store, local files and existing dev and shared-server behavior. Extension checks are inpackages/server/test/extension.test.ts.