Ways in

Other programs reach an account five ways, and each is deliberately narrow. Nothing here runs code inside ontoplano: there is no plugin that is a file you drop in, and that is on purpose — a plugin platform made of processes on somebody's own machine cannot take the instance down with it.

API tokens

A token is created in Settings → AI & Integrations → Integrations, shown once, and stored as a hash. It carries scopes, and a scope is a sentence somebody agreed to rather than a permission bit: a grant given to a string of jargon is not informed.

Scope What granting it allows
streams:write Send readings into your data streams, and create new streams
streams:read Read everything your data streams have ever recorded
schedule:read Read everything on your calendar for the days ahead, today included
schedule:write Put blocks on your week — one-off and repeating — move and rename them, take them off a day, set reminders on them, and mark them done or skipped
today:read See today's plan — the blocks and the tasks on it
reminders:read See the reminders you have coming, so this device can ring for them
habits:read See your habits, which are due today, and whether you kept them
habits:write Mark a habit kept, or unmark one
plugin:declare Name and describe itself on your integrations page
webhooks:manage Send itself a message when something changes here — a task finished, a block done — to an address it chooses
audio:read Read your recordings and their notes
audio:write Rename your recordings and write their notes
inventory:read See everything you keep and everything on your shopping list
inventory:write Add things, tick them bought, change how many you keep, and take things off
locations:read See where your things live, and what is in each room and drawer
locations:write Add and change rooms and drawers, and say where a thing lives
calendar:read Show your plan in a calendar app. It can see the plan and change nothing
notes:read Read your diary and your notebooks, and the pictures and recordings in them
notes:write Write in your diary and your notebooks
ideas:read See your ideas, and the pictures and recordings in them
ideas:write Add ideas, change them, and remove them
tasks:read Read your todo list and your goals, and the pictures and recordings on them
tasks:write Add, finish and delete todos, move them on and off a day, and close a goal
kitchen:read Read your recipes, and their photographs
kitchen:write Add and change recipes
workouts:read See your workouts
workouts:write Add and change workouts and their categories, put them away, and mark one done
bills:read See your bills, your recorded income, and what you have actually paid or received
statements:read Read your imported bank-statement lines, their categories and tags, and the monthly in/out figures
statements:write Add and remove the rules that sort your statement lines
bills:write Add and change bills, and mark them paid
people:read See the people in your life, whose birthday is coming, and their photographs
people:write Add people, and change what is recorded about them
search:read Search everything you have written, in one go
tags:read See the labels you use, and the colours you gave them
tags:write Rename your labels, colour them, and merge two into one
destructive Delete things outright — with only the write grants, it can add and change but never remove
notebooks:deliver File things into your other notebooks, and move this notebook’s things there — seeing only their names, never what is in them

The narrowness is the point. A phone pushing weight readings needs to write to a stream and read the schedule; it has no business reading the diary if that phone is ever lost.

The one exception to "shown once". A calendar app cannot send a header, so the credential has to live in the URL — which means it is written into config files, walked past by every proxy in between, and sometimes shared with a household.

Two things keep that bounded. It holds calendar:read and nothing else, and the feed route accepts a token holding exactly that — a wider-scoped token pasted into a feed URL is refused rather than honoured. And because what it can do is so small, the address is kept and can be shown again, which is what makes adding a second device possible without breaking the first. Up to five, each revoked on its own.

Data streams

An external program declares a stream and pushes points into it. The app decides how to draw them; the program decides nothing about the UI. This is how anything that measures the world — a scale, a sensor, a script — gets onto the dashboard without shipping any code into ontoplano.

Webhooks

The other direction: a subscription is an address, a set of events, and a secret the delivery is signed with, so the receiver can tell it is really us.

Event When it fires

A subscription that keeps failing is disabled rather than retried forever, and says so on the settings page where it can be revived.

An AI assistant, over MCP

POST /api/mcp is a Model Context Protocol server over these same tokens and scopes — the fifth way in, and the only one where the thing at the other end decides what to do. It has a page of its own.

Two plugins to read

Both are in ontoplano-plugins/, both are plain Node with no dependencies, and neither runs anything inside anybody's server.

onto-morning.mjs is the short one, and the place to start: a single token holding one scope, one GET of /api/v1/today, and a message to ntfy or Telegram from a crontab line. Around 150 lines including the comments. It is what most plugins look like.

onto-household.mjs is the two-way one: it keeps two accounts' shopping lists equal over webhooks — add milk on one phone and it is on the other, tick it in the aisle and it is bought on both — with signature verification and idempotent subscription. The two accounts can be on different instances, which is the thing no shared-table design can offer.

The server makes no request on your behalf

There is no place in ontoplano where you hand it an address and it goes and fetches it. That is a deliberate absence rather than a gap.

Importing a recipe from a link would be the obvious place for one, and it was built that way once. A request made from inside the box reaches everything the box can reach and nothing outside it can — on a rented VPS the cloud provider's metadata service, which hands out credentials to whatever asks; on a home server the router's admin page, the NAS, and this app's own port. That is server-side request forgery, and it is defensible: check the resolved address rather than the string, refuse every private range in v4 and v6, re-check each redirect, cap the body and the clock, and say the same sentence about every refusal so the messages cannot map a network.

It was all of that, and it was still a door, on machines belonging to people who did not ask for it. So the recipe import takes the page as a paste instead: select all on the recipe page, copy, paste. It reads the same schema.org data, works on sites that refuse servers anyway, and there is nothing left to guard.

Outbound requests ontoplano does make are ones the operator configured — webhooks to an address in your own settings, ntfy, mail. Those are a different thing: the address comes from the person running the instance, not from whoever can sign in to it.