Enterprise

Multi-tenancy

Projects, membership, and isolation guarantees.

The project is the tenancy boundary in Neuralis — think of it as a department: its own members and roles, its own agents, conversations, filesystem sources, package enablement, and spending limits. One instance serves many such teams, each project's data lives in its own directory tree under the per-project zone, and nothing is shared across projects unless an admin explicitly configures it at platform scope. Crucially, the boundary holds for inference too: to a non-member, a project's resources do not merely refuse access — they do not appear to exist.

Membership and roles

A project record stores its members as a map of user id → membership entry: the user's role in this project, a display position, and when they were added. The same user can be an owner in one project and a viewer in another — roles are always project-local.

Inviting users and changing roles are privileged operations:

  • only roles with the invite capability can add members,
  • role assignment is priority-gated: nobody can hand out a role stronger than their own (see roles and features),
  • owner-membership invariants are enforced server-side, so a project cannot be edited into an ownerless state.

Isolation guarantees

  • Every operation is scoped. Routes and tools execute against an authenticated userId + projectId (and optional agentId); a project id is derived from the verified session, never trusted from request parameters.
  • Non-members cannot infer existence. A user who is not a member of a project gets no metadata about it — not its packages, widgets, skills, sources, agents, or runs. Workspace snapshots are filtered server-side by membership and feature grants before they leave the host.
  • Members read a feature-keyed view. Even inside a project, the record is projected per caller: other members' email addresses, other roles' feature grant lists and other members' per-user spend caps each require their own read feature. Everyone always sees their own row, their own role's grants and their own cap; a role with management rights reads the full record.
  • Revocation applies mid-stream. Skill scripts authenticate with a per-stream session ticket, and the host re-checks project membership on every ticketed call — a member removed while an agent is running loses access on the next call, not at the next login.
  • Data is physically separated. Each project's files, agent configurations, conversations, and source configs live under that project's own directory; vector memory is partitioned by project-scoped sources.

These guarantees are layers two and three of the platform-wide security model.

Notifications

When something a person should hear about happens outside a live conversation — a workflow run finishes or fails, a workflow is paused automatically, a source stops syncing — the platform records a notification for the people it concerns: by default the workflow's creator or the source's owner. Packages declare these events in their manifest; the history, the unread counts and the settings are a platform service.

  • Per project, per person. Each member's notification history lives inside that project's own data tree, never crosses into another project, and is removed with the project when it is permanently deleted.
  • A signal, never content. A notification carries ids, a title and a fixed status — never an error message, a transcript or a path. The detail stays behind the workflow's or source's own access checks.
  • Checked again on every read. A listed notification is re-checked against the reader's current access, so a member who loses access to a package or a workflow stops seeing its notifications.
  • Unread counts are kept per package dock entry and per project — numbers only, never a title.
  • Personal settings. Each member can mute an event type, or follow one they are not addressed by, such as a teammate's workflow failing. Following never widens what a member may see: only event types they could already receive are accepted, and every event still passes the same checks for them.

Members see these as count badges on dock entries and in the Inbox in the account menu — see the workspace.

Two platform settings bound the history: notificationsMaxRowsPerUser (500 rows per member per project by default) and notificationsRetentionDays (30 days by default). Older rows are dropped when the member's next notification is written; there is no background sweep. A member can also clear their own notifications — one app's, or all of them — at any time; that deletes them for that member only and never touches anyone else's.

Per-project limits

Each project carries USD spend limits, configurable from the admin widget's Limits tab. Every rule sets an amount and exactly one period — daily, weekly or monthly (UTC calendar windows):

  • a project-wide total,
  • per-role, per-user, and per-agent caps,
  • an optional per-minute limit on provider calls.

The per-minute limit counts PROVIDER CALLS, not conversations: one answer makes one call per step it works through, and subagents and background helpers count against the same allowance. Reaching it does not fail the answer — the turn waits for the next minute and carries on, showing a countdown while it waits, so a long piece of work is never thrown away to enforce a rate and a waiting turn is never mistaken for a dead one.

The most specific configured limit applies, checked in the order user → role → agent — and the project total is always enforced as a ceiling on top of the matched rule; an unset limit means unlimited at that level.

Editing limits is itself governed: tightening a cap is open to anyone who can reach the write, but loosening or removing a rule requires strength — a strictly stronger role than the row's target for per-role and per-user caps (nobody can loosen their own or a peer's), and admin strength for the project total and per-agent rows. Looser versus tighter is judged on the day-normalized rate, so switching a rule's period can legitimately count as a tightening. Spend is counted as an estimate priced from the model catalog's list rates, never a negotiated or promotional rate — so for any model the catalog prices, a cap closes at or before the money is actually spent. Raising a cap takes effect on your next message: the current window's spend is measured against the new figure at once, not from the next window. A turn already running keeps the limits it started with. A stopped answer is still charged for what the provider already read: the step that was cut books the numbers the provider reported, or the size of the last measured request as its input. On providers that report nothing until an answer finishes, a stopped step's output is not billed — the usage record says so rather than presenting the gap as zero cost. A cap that is reached mid-answer stops the turn and says so: the banner names which limit closed, its amount and how much of it has been used — to enough precision to read even when the figures are fractions of a cent — instead of the answer simply ending. The check runs before every step of a turn, including the first, and turns running side by side for the same person see each other's spend as it happens rather than only once each finishes. Auxiliary model calls (titles, summaries, prompt polish) count toward the totals; subscription-priced models' own steps sit outside these caps. Non-LLM API spend — embeddings included — is governed separately by per-credential use limits (calls per period rather than dollars); embedding additionally has its own platform-wide daily dollar cap, embeddingDailyUsdCap (see memory and sync).

Creating projects

Spinning up a new project is itself a privileged act, because each project is a new tenant:

  • owner-strength only — only a user who already holds an owner-level role in at least one existing project may create a new one; ordinary members and admins cannot. (The very first project is provisioned during setup.)
  • an instance-wide cap — the admin-configurable Max Projects setting bounds the total number of projects across the whole instance (0 = unlimited). The cap is enforced instance-wide, not per user, and is checked only after the creator is authorized, so an unauthorized caller never learns the count.

Both checks run server-side on the create request, and a successful creation is recorded in the audit log. Creating a project is done from the admin dashboard's Projects tab.

Deleting projects

Removing a tenant is a two-stage, owner-strength operation, so an accidental click never destroys data and offboarding is auditable. "Owner-strength" means a role at priority 1 in that project — the built-in owner, or any role you define at that priority; the record's creator field grants nothing on its own:

  • Archive (soft-delete). Deleting a project first archives it: the project is hidden from every listing and disabled — members lose access (package routes return "not a member") and its scheduled workflows stop running and stop spending. Nothing is erased, and an archived project can be restored at any time, which re-enables it exactly as before.
  • Delete permanently. Only an already-archived project can be permanently deleted, from the Projects tab behind a typed-name confirmation. This is irreversible: it removes the project's virtual desktops together with their browser profiles, all of its vector memory, its data tree, its source configurations, and its project- and agent-scoped credentials. If a desktop or the vector store cannot be cleaned up, nothing else is removed: the project stays archived and the deletion can simply be retried. A deleted project's id is never given to a new project, so a new tenant can never inherit anything the old one left behind. External and mounted source content is never touched — permanent deletion only removes data that lives inside the project's own zone; a folder or host path you mounted into the project keeps its files, and credentials scoped to a user or to the whole platform are left intact.

Both archive and permanent deletion require an owner-strength role and are recorded in the audit log.

Per-project packages

Beyond the builtin packages the platform admin installs for everyone, each project can install its own packages into the project's _packages/ directory. Project packages are a separate trust class:

  • they default to untrusted and run sandboxed,
  • a member whose role can manage packages (the role-management flag, or the package-management feature) can raise an individual package to trusted — recorded per project, never globally,
  • a project package can never shadow the id of a loaded builtin package,
  • two projects (or two users/agents) can each install a package with the same name without colliding — the runtime keys every project package by its owning project/user/agent scope, so one tenant's package can never substitute another's code or leak across the boundary,
  • which packages are enabled is itself project-scoped configuration, and a disabled package disappears from agents' context and tool lists.

See the package system for the two package classes and their lifecycle.

Agent scoping

Agents belong to a project, and access to them is part of the role definition: a role grants * (all agents), own (only agents the user created or is assigned to), or view. Ownership is proven two ways: the project record can hold an assignment entry per agent — who created it and which members it is assigned to — and, where no such entry exists, the agent's own recorded creator. So an agent you create is yours to manage straight away, while an explicit assignment always takes precedence over the creator claim. Agent provisioning is priority-gated like any other role assignment, so a member cannot create an agent that acts with admin-strength identity.

A project also has a maximum number of agents, which an administrator sets. It bounds every way a person or an agent can create one; internal platform provisioning is exempt, so it never blocks the platform's own automation.

Each agent has its own data zone and conversation history, and credentials can be scoped to a single agent — the most specific scope in the credential resolution chain.

On this page