Skip to main content

Sign Up

Creates a new user account on the cloud backend and logs it in, with inputs for custom user properties.

Sign Up creates a user from the current username, password and optional email (string values), plus one input per custom property on the User class, when signup (Do, signal) is triggered. On success the new account is immediately logged in — the session is stored in the browser, every User node updates, their Logged In signal fires — and done (signal) fires. On failure (e.g. username taken) error (string value) is set and failure (signal) fires, and no account is created.

When to use it​

The action node behind a registration form. For signing in an existing user use Log In; for editing the signed-in user's properties later use Set User Properties.

At a glance​

CategoryCloud Services
Type namenet.noodl.user.SignUp
Available inbrowser
SSR compatibilitysafe
Provided bynoodl-viewer-react
Behaviour specversion 2 (under the hood)

Inputs​

Values​

NameTypeDefaultDescription
emailString—Email address for the new account; leave blank if the project does not ask for one
passwordString—Password for the new account
usernameString—Username for the new account

Signals​

NameTypeDefaultDescription
signupSignal—Creates an account from the values below and signs it in

What each input does​

Do (signup)

  • the first press this frame: the call goes to the backend at the frame's end, with the values held then
  • another press this frame: it joins that call, and is answered with it

Username (username)

  • always: held as handed; read when the next sign-up goes

Password (password)

  • always: held as handed; read when the next sign-up goes

Email (email)

  • always: held as handed; read when the next sign-up goes

Outputs​

Signals​

NameTypeDefaultDescription
completedSignal—Fires after every invocation, whatever the outcome — wire this to carry on regardless. Failure still fires and still carries its reason, so this cannot hide an error
doneSignal—Fires once the account has been created and signed in

Failure outputs​

NameTypeDefaultDescription
errorString—Why the last sign-up failed; empty until one does
failureSignal—Fires when the account could not be created, after the reason has been reported on the error channel

Dynamic ports​

This node's port list changes at runtime (runtime-discovered); the tables above may be incomplete for a given instance.

Property inputs include one port per custom user property.

Ports at runtime​

The editor generates one value input per custom property in the User class schema (stored as prop-<name>, typed from the schema; built-ins like username, password, email and system fields are excluded). These set the new user's properties at creation time.

Patterns​

  • Text Inputs → username/password/email, Button onClick → signup, done → navigate into the app: the standard registration form.
  • Fill prop-<name> inputs at sign-up for properties every account must have, instead of a follow-up Set User Properties call.

Watch out for​

  • Triggering Log In from done — sign-up already establishes the session.

Examples​

Sign up, then set profile properties

Sign Up creates the account and starts a session (done fires once the user is signed up and logged in). Custom profile fields beyond username/email/password are written afterwards with Set User Properties, whose property inputs are generated from the user-class schema — here displayName is set from the same form, triggered by the signup succeeding. Chain the signals: signup done → set-properties store.

Log In, User, Set User Properties

Code export​

Exports to React. A project that uses it exports this node as React code.

How export works shows what an export writes, with a worked example.

Under the hood​

This node's behaviour is written down as an executable spec in @nodegx/node-spec, version 2. The spec is the reference the conformance tests grade the runtime against. Every rule in it cites the line of the runtime it was read from. The full spec is at the bottom of this page.

What it depends on outside the node​

  • a backend: records, users and cloud functions (backend)
  • the shared store of Objects and Arrays, looked up by id (registry)
  • time: timers, delays and the frame clock (clock)

Starting state​

{
"properties": {},
"scheduled": false,
"presses": 0,
"calls": {},
"nextCall": 1
}

How actions report back​

Each time Do is triggered, the node reports exactly one of Done or Failure.

Version history​

  • Version 2: On the Parse wire the accounts class's ACL column draws no input port any more, as no Record node draws one: it is the account's access rules, not something about the person. Before, the Parse wire drew prop-ACL here (and only on the User family). Existing projects are not migrated: ruled "Hide it, like the Record nodes" (R29). The reach measured on 2026-10-12 was 429 User-family nodes in 58 of Richard's projects and none wired to or from ACL, so no saved wire is dropped.

The full spec​

The spec's source, as the docs were generated. Helpers it imports live beside it in packages/nodegx-node-spec/src.

packages/nodegx-node-spec/src/nodes/user-actions.ts
/**
* Log In (`net.noodl.user.LogIn`), Sign Up (`net.noodl.user.SignUp`) and Log Out (`net.noodl.user.LogOut`) — read from
* `packages/noodl-viewer-react/src/nodes/std-library/user/{login,signup,logout}.ts` on 2026-10-02 (NSP-014 s27), with the
* viewer's `userservice.ts` and `RestAuthAdapter.ts` beside them. The three nodes that MOVE the session the User node shows
* (user.ts): one shape, three operations, so one factory.
*
* R3 (a): the runtime wins by default. Every rule below cites the line it was read from (login.ts unless named).
*
* HELD VALUES: Username and Password (Log In :84-98, Sign Up :105-120) and Sign Up's Email (:121-129) are held as they
* arrive, no conversion. Sign Up's `prop-<field>` inputs (`registerInputIfNeeded` signup.ts :185-195 — the accounts table's
* columns, from the project's schema; a play declares `nick` and `n`) are held in ONE object the node keeps for its life
* (:72, :180-182 — a value is never removed, `undefined` included). Log Out holds nothing. None of the three has a Backend
* input: every call goes to the ACTIVE backend (userservice.ts :357-365, `backendId` absent).
*
* DO (:75-80 → :126-161): one token per press, minted before the guard; ONE call per frame for any number of presses
* (`logInScheduled`). At the frame end the node reaches the service (`UserService.instance`, :143 — the first reach in a
* play makes it, and its start-up check, world.ts AUTH) and hands it the operation with the held values AS THEY ARE AT THE
* FRAME END: `logIn({ username, password })`, `signUp({ username, password, email, properties })`, `logOut({})`. The batch is
* taken before the call goes out (:140-141): a press while the call is in flight is the next frame's batch, its own call.
* Success: every press of the batch Done (:146-148). Failure: Error is the message AS GIVEN — no fallback sentence, so a
* failure with none sends nothing on Error (`undefined` is never sent, CONTRACT C3) — and every press Failure with the node's
* code (:149-151 → `setError` :109-117). Error is never cleared. No Unchanged on any of the three (:57-58; signup.ts :79-80;
* logout.ts :48-51).
*
* WHAT THE CALL DOES TO THE SESSION is the backend's (world.ts AUTH, s27): a sign-in or sign-up that succeeds REPLACES it
* with the answer and announces `loggedIn` after the Done; a sign-out clears it WHATEVER the backend answers and announces
* `loggedOut` — so on every backend here Log Out's Failure and Error never fire (the adapter's `finish` on both `ok` and
* `fail`, RestAuthAdapter.ts :1064-1083), and with nobody signed in it is Done inside the call (:1070-1073). A User node
* shows it, graph-side (graph s10).
*
* NOT GRADED HERE, named:
* - the capability gate (`begin`) and each wire's own steps inside a sign-in — world.ts AUTH "Not played";
* - Sign Up's ports as the editor offers them (`updatePorts` signup.ts :198-252, from the `systemCollections` metadata —
* a viewer-with-editor path, NSP-020's);
* - the editor's warnings (`clearWarnings`) and the error-channel raise — not ports.
*/

import type { BackendAnswerEvent, ValueInputDecl, WorldView } from '../spec';
import { defineNode } from '../spec';
import type { BackendScript } from '../world';
import { recordFieldType } from './record-base';
import { USER_FIELDS, userFields } from './user';

/** signup.ts :185-195 — a `prop-<field>` port, held by its field. */
const signUpProperty = (field: string): ValueInputDecl => ({ type: '*', coerce: 'none', displayName: field, group: 'Properties', description: 'Value of ' + field + ' for the new account', examples: ['Annie', 2, '', null] });

/** login.ts :22, signup.ts :30, logout.ts :22 */
export const LOG_IN_ERROR_CODE = 'user/log-in-failed';
export const SIGN_UP_ERROR_CODE = 'user/sign-up-failed';
export const LOG_OUT_ERROR_CODE = 'user/log-out-failed';

type ActionState = {
username: unknown;
password: unknown;
email: unknown;
/** Sign Up's `_internal.userProperties` — every `prop-` value ever written, by field */
properties: Readonly<Record<string, unknown>>;
/** `_internal.error` (:110) */
error: string | undefined;
/** `logInScheduled` / `signUpScheduled` / `logOutScheduled` */
scheduled: boolean;
/** `pendingLogIn.length` — the presses the frame's batch holds (:129-130) */
presses: number;
/** every call made, by the spec's call id: how many presses its answer settles */
calls: Readonly<Record<string, number>>;
nextCall: number;
};

const INITIAL: ActionState = { username: undefined, password: undefined, email: undefined, properties: {}, error: undefined, scheduled: false, presses: 0, calls: {}, nextCall: 1 };

/** The account a world signs in: the user's fields flat, as the adapter flattens a sign-in's answer (world.ts AUTH). */
export const ANN = { objectId: 'u1', email: 'ann@example.com', username: 'ann', nick: 'Ann', n: 1 };
const BEA = { objectId: 'u2', email: 'bea@example.com', username: 'bea', nick: 'Bea', n: 2 };
/** The service's start-up check, never answered — the pools that hold a session at the start script it so it is not a violation. */
export const NO_CHECK = { match: { op: 'fetchCurrentUser' }, answer: { never: true as const } };

/** The backends a sequence plays with, for one operation: it succeeds at once and late, is refused with and without a message, is never answered — with nobody signed in and with somebody. */
export function pool(op: string, ok: unknown): ReadonlyArray<BackendScript> {
return [
{ answers: [{ match: { op }, answer: { ok } }] },
{ answers: [{ match: { op }, answer: { ok }, after: 20 }] },
{ sessions: { main: ANN }, answers: [NO_CHECK, { match: { op }, answer: { ok } }] },
{ answers: [{ match: { op }, answer: { error: 'Invalid credentials.' } }] },
{ answers: [{ match: { op }, answer: { error: null }, after: 1 }] },
{ sessions: { main: ANN }, answers: [NO_CHECK, { match: { op }, answer: { error: 'HTTP 503: {}' }, after: 5 }] },
{ answers: [{ match: { op }, answer: { never: true } }] }
];
}

export const stringInput = (displayName: string, description: string, examples: unknown[]): ValueInputDecl => ({ type: 'string', coerce: 'none', displayName, group: 'General', description, examples });

export const failures = <P extends string>(port: P, code: string, n: number) => Array.from({ length: n }, () => ({ port, outcome: 'failure' as const, error: code }));
export const dones = <P extends string>(port: P, n: number) => Array.from({ length: n }, () => ({ port, outcome: 'done' as const }));

/** :78-80 → :126-133 — a token per press, minted before the guard; one call per frame */
const press = (s: ActionState) =>
!s.scheduled
? { says: 'the first press this frame: the call goes to the backend at the frame\'s end, with the values held then', set: { presses: s.presses + 1, scheduled: true }, send: [] as never[], outcome: 'pending' as const }
: { says: 'another press this frame: it joins that call, and is answered with it', set: { presses: s.presses + 1 }, send: [] as never[], outcome: 'pending' as const };

/** :135-160 — the frame's batch: one call, to the active backend, with the held values as they are now */
function batch(s: ActionState, w: WorldView, op: 'logIn' | 'signUp' | 'logOut', args: Record<string, unknown>) {
if (!s.scheduled) return { send: [] as never[] };
const n = s.presses;
w.userService(); // :143 — `UserService.instance`: the first reach makes the service
const backend = w.backendFor(undefined) as string; // userservice.ts :357-365 — no `backendId`: the active one, never none
const id = op + ':' + s.nextCall;
return {
set: { presses: 0, scheduled: false, calls: { ...s.calls, [id]: n }, nextCall: s.nextCall + 1 },
send: [] as never[],
backend: { id, op, backend, args }
};
}

/** :146-151 — Done for every press of the batch; or Error as given (:109-117) and Failure with the node's code */
function answered<P extends string>(s: ActionState, answer: BackendAnswerEvent, port: P, code: string) {
const n = s.calls[answer.id] ?? 0;
if (!('ok' in answer)) return { set: { error: answer.error }, send: ['error' as const], outcomes: failures(port, code, n) };
return { set: {}, send: [] as never[], outcomes: dones(port, n) };
}

const errorOutput = (description: string) => ({ type: 'string' as const, displayName: 'Error', group: 'Error', description, from: (s: ActionState) => s.error }); // :63-71

export const LogIn = defineNode({
type: 'net.noodl.user.LogIn',
version: 1,
source: 'packages/noodl-viewer-react/src/nodes/std-library/user/login.ts; userservice.ts; RestAuthAdapter.ts',
needs: ['backend', 'registry', 'clock'],
worldPool: { backends: pool('logIn', BEA) },
state: INITIAL,
outcomes: ['done', 'failure'],
inputs: {
// :75-81
login: { type: 'signal', outcome: true, displayName: 'Do', group: 'Actions', description: 'Attempts to sign in with Username and Password' },
username: stringInput('Username', 'Username to sign in as', ['bea', '']), // :82-90
password: stringInput('Password', 'Password to sign in with', ['secret', '']) // :91-99
},
outputs: { error: errorOutput('Why the last sign-in failed; empty until one does') }
}).on(
{
login: press,
username: (_s, v) => ({ says: 'always: held as handed; read when the next sign-in goes', set: { username: v }, send: [] }),
password: (_s, v) => ({ says: 'always: held as handed; read when the next sign-in goes', set: { password: v }, send: [] })
},
{
afterInputs: (s, _i, w) => batch(s, w, 'logIn', { username: s.username, password: s.password }), // :143-146
world: { backend: (s, _i, answer) => answered(s, answer, 'login', LOG_IN_ERROR_CODE) }
}
);

export const SignUp = defineNode({
type: 'net.noodl.user.SignUp',
// v2 (P107 s92, P107-S84-USERACL ruled R29): no `prop-ACL` input (signup.ts `_ignoreKeys` names ACL)
version: 2,
changes: [
{
version: 2,
says: 'On the Parse wire the accounts class\'s ACL column draws no input port any more, as no Record node draws one: it is the account\'s access rules, not something about the person. Before, the Parse wire drew prop-ACL here (and only on the User family)',
row: 'P107-S84-USERACL (R29)',
migration: {
none: 'Ruled "Hide it, like the Record nodes" (R29). The reach measured on 2026-10-12 was 429 User-family nodes in 58 of Richard\'s projects and none wired to or from ACL, so no saved wire is dropped'
}
}
],
source: 'packages/noodl-viewer-react/src/nodes/std-library/user/signup.ts; userservice.ts; RestAuthAdapter.ts',
needs: ['backend', 'registry', 'clock'],
worldPool: { backends: pool('signUp', BEA) },
state: INITIAL,
outcomes: ['done', 'failure'],
inputs: {
// signup.ts :97-103
signup: { type: 'signal', outcome: true, displayName: 'Do', group: 'Actions', description: 'Creates an account from the values below and signs it in' },
username: stringInput('Username', 'Username for the new account', ['bea', '']), // :104-112
password: stringInput('Password', 'Password for the new account', ['secret', '']), // :113-121
email: stringInput('Email', 'Email address for the new account; leave blank if the project does not ask for one', ['bea@example.com', '']) // :122-130
},
outputs: { error: errorOutput('Why the last sign-up failed; empty until one does') }
}).on(
{
signup: press,
username: (_s, v) => ({ says: 'always: held as handed; read when the next sign-up goes', set: { username: v }, send: [] }),
password: (_s, v) => ({ says: 'always: held as handed; read when the next sign-up goes', set: { password: v }, send: [] }),
email: (_s, v) => ({ says: 'always: held as handed; read when the next sign-up goes', set: { email: v }, send: [] })
},
{
derived: {
// NSP-020 s84 — what the editor draws (signup.ts `updatePorts` :199-250, from `systemCollections`): one `prop-` per column
// of `_User` less the ones a sign-up does not set and every Relation ("cannot be set at sign-up time"), typed by the
// Parse table. No Backend picker. At play there is no schema: a write registers each (:185-195)
inputs: (_params, context) => {
const out: Record<string, ValueInputDecl> = {};
for (const [p, decl] of userFields(context, ['authData', 'password', 'username', 'createdAt', 'updatedAt', 'emailVerified', 'email'], '_User')) out['prop-' + p] = { ...signUpProperty(p), type: recordFieldType(decl) };
return out;
},
needs: ['schema'],
// signup.ts :185-195 registerInputIfNeeded — any `prop-` name, held by its field
discover: (port) => (port.startsWith('prop-') ? signUpProperty(port.slice('prop-'.length)) : undefined),
candidates: USER_FIELDS.map((f) => 'prop-' + f),
on: (s, port, v) => ({ set: { properties: { ...(s as ActionState).properties, [port.slice('prop-'.length)]: v } }, send: [] }) // :180-182
},
afterInputs: (s, _i, w) => batch(s, w, 'signUp', { username: s.username, password: s.password, email: s.email, properties: s.properties }), // signup.ts :166-170
world: { backend: (s, _i, answer) => answered(s, answer, 'signup', SIGN_UP_ERROR_CODE) }
}
);

export const LogOut = defineNode({
type: 'net.noodl.user.LogOut',
version: 1,
source: 'packages/noodl-viewer-react/src/nodes/std-library/user/logout.ts; userservice.ts; RestAuthAdapter.ts',
needs: ['backend', 'registry', 'clock'],
worldPool: { backends: pool('logOut', null) },
state: INITIAL,
outcomes: ['done', 'failure'],
inputs: {
// logout.ts :67-76 — named `login`: persisted in every project, never corrected
login: { type: 'signal', outcome: true, displayName: 'Do', group: 'Actions', description: 'Signs the current user out and clears the stored session' }
},
outputs: { error: errorOutput('Why the last sign-out failed; empty until one does') }
}).on(
{ login: press },
{
afterInputs: (s, _i, w) => batch(s, w, 'logOut', {}), // logout.ts :110-117 — the callbacks only
world: { backend: (s, _i, answer) => answered(s, answer, 'login', LOG_OUT_ERROR_CODE) }
}
);
Generated

This page is generated from node-catalog-enriched.json, the node's spec in packages/nodegx-node-spec and the export ledger. Do not edit it by hand — run npm run docs:nodes to regenerate, and fix the source instead.