weft-members

Read when a program serves several PEOPLE, each with their own account, number, bot, spreadsheet or schedule (a WhatsApp assistant for many customers, a Slack bot per workspace, a report per client):

SKILL.md

GitHub

Programs for several people

A [member] is one person the program serves. weft knows nothing about people: it knows [instance]s, separate running copies of part of the program under their own ids (the weft-instances skill). Which person owns which instances is the program's own data, kept in a table in its own database. Everything a person does reaches weft through one of their instances.

You never build one project per person, and you never ask each person for a key in a form field: their accounts are @instance_filled connections they connect on their own settings page.

Keeping who owns what

The program keeps one table, in the Postgres the program and its frontend share (the weft-frontend skill has the one shared database):

create table if not exists member_instances (
  user_id     text not null,
  instance_id text not null primary key,
  label       text,
  created_at  timestamptz not null default now(),
  last_used   timestamptz not null default now()
);
create index if not exists member_instances_user on member_instances (user_id);

last_used is the column the idle cleanup reads (under "Every instance needs an end" in weft-instances); every run for an instance updates it.

One instance per person is the simple case and the default: the instance id is the person's user id (their BetterAuth id on a site you build), so the table holds one row per person and the lookup is trivial. Use it whenever a person needs exactly one copy (one WhatsApp number, one workspace).

Several instances per person (a session per chat, a bot per workspace they run, a sandbox per job): the program mints a fresh id per instance (<user id>-<random> reads well in weft status), writes a row, and:

  • listing a person's instances is a query on user_id, run from a gated route the frontend's server calls after reading the session;
  • opening one is a check that the row belongs to that person, then MintInstanceToken for that one instance id, and the page uses that token;
  • removing one is the same check, then WipeInstance and deleting the row.

The check against the table happens on every call that names an instance: a person can only ever reach an instance whose row carries their user id.

The shape you build

Propose these parts every time, before any node is written. The routes are shared (never per instance), as weft-instances explains: a route sits outside every group that receives a per-instance value, and sends its work into that group through the group's inputs.

  1. The per-instance part: what you mark @instance_filled (and @per_instance on an infra node), and the nodes that follow. When an instance's trigger reads a value filled per instance (a schedule, a sheet), give that field a fallback if one makes sense, so the trigger can be turned on the moment the instance is created.
  2. An admin route the frontend's server calls, gated by ApiKeyAuth on the Route's auth, its key only in that server's environment. It onboards a person: writes the row, MintInstanceToken, then, if there is a per-instance infra node, StartInfra with waitUntilRunning off, then Reply with the token so the request ends at once, then a second StartInfra left on and ActivateTriggers. A Reply before the start would leave the person's container out of a list read right after it. The same route (or siblings on the same gate) lists a person's instances, mints a token for the one they open, and removes one with WipeInstance.
  3. A settings page where each person fills in what the program asks of their instance: their connections, their spreadsheet, their model, their schedule. It is InstanceSettings on the frontend with that instance's token (the weft-frontend skill mounts it), or the browser extension's Your settings page. If a trigger needs a value with no fallback, the page calls the admin route again to turn the triggers on once the values are saved.
  4. The cleanup cron from weft-instances, with the idle limit the user chose.

A person's own runs go through the program's shared routes: the frontend's server calls a gated route with Weft-Instance: <instance id> after checking the table, or a page calls a route with that instance's token in Weft-Instance-Token.

Connections per person

Each person connects their own accounts when the access node's field is written account: @instance_filled: that person's calls then run on their key and their bill. A field left unmarked runs every person on the connection the user picked on the install, on the user's bill. Ask the user which they want for each paid service before you write it.

To try a person's path before any website exists, give a test instance the values a person would: weft connect --instance alice --node <node> and weft instance-values --instance alice --set <node>.<field>=<value>, then run with --instance alice.

Billing people

If the site bills its people, a gated route reads InstanceCosts for each of that person's instances (from the table) and the frontend shows or charges the sum. weft only records the cost.