Organizations
architect-orgs: the person's organizations, the window's current one or All, roles and capabilities, the switcher, everything stored under the org, and servers confined to the caller's org.
What it is and when to use it#
The organization is architect’s concept, not each app’s. One identity server
(fts-auth) owns orgs, members and roles. A window is in one org, or in All.
Every layer stores an org’s data under OrgId::path (org/<id>/…), writes
only to the scope’s write target, and reads All with read_all, where one
org failing hides none of the others. Roles map to the app’s capabilities
(notes.write), with inheritance and per-org grants. On a server, an org
lane’s scope (Lane::new(router).scoped(org) on an EngineHost, confine
elsewhere) puts each lane in one org, entered outside the gate, and ConfinedEngine refuses non-members and
resources under another org.
Use it for the org switcher, capability checks, All-organizations views and server tenancy. Keep the org out of method arguments: it comes from the lane, never from the caller. Design: Organizations.

A minimal example#
The default build is the model and scope (wasm-clean); auth adds fetch;
ui the hooks and switcher; commands ⌘O across orgs; server confinement.
Client side, as in apps/ui/src/lib.rs:
let caps = Capabilities::new()
.allow(&Role::Viewer, ["notes.read"])
.allow(&Role::Member, ["notes.write"])
.inherit();
let org = architect_orgs::use_orgs_provider(|| cached_orgs, caps, None);
use_orgs_live(org, conn, move |c| {
let session = session.clone();
async move {
architect_orgs::fetch(&OrganizationServiceClient::new(c), &session)
.await
.map_err(|e| e.to_string())
}
});
let workspaces = org.switcher(Some("All organizations"));
// In a feature: use_org() gives scope() and can("notes.write")
Server side, from features/orgs/tests/host_lane.rs and
features/orgs/tests/server.rs:
use architect::host::{EngineHost, Lane};
use architect_orgs::server::{ConfinedEngine, Members, MembersResolver, confine};
let members = Members::new();
let identity = MembersResolver::new(sessions, directory, members.clone());
let engine = ConfinedEngine::new(members.clone())
.role(&Role::Member, vec![Rule::new("**", &["read", "write"])]);
let gate = PermissionsGate::new(Arc::new(engine), Arc::new(identity))
.permit(notes_descriptor(), &PERMITS);
// On an EngineHost, /org/{slug}/vox: the host gates inside the lane's org.
EngineHost::new(router.clone(), gate, bind).vox_at("/org/{slug}/vox", move |head| {
let router = router.clone();
async move { Ok::<_, StatusCode>(Lane::new(router).scoped(org_of(&head)?)) }
});
// Or per connection on a transport of your own (only a gated handler fits):
let handler = confine(OrgId::new(slug), PermissionsGate::wrap_shared_with_bearer(gate, router, bearer));
// In a handler: current_org(); keys: org.path("notes/…")
directory is the identity server; IdentityServer(orgs_client) wraps an
OrganizationService client as one.
Code tour#
features/orgs/src/model.rsOrgs(switch,refresh,can),Selection(One,All),Role,Capabilities(allow,inherit,grant).features/orgs/src/scope.rsScope(writes_to,is_all),OrgId::path,OrgId::doc_id,read_allandAggregate.features/orgs/src/auth.rsfetch,from_bundles: the person’s orgs from the identity server.features/orgs/src/refresh.rsRefresh: one fetch per connection, the cache kept when it fails.features/orgs/src/ui.rsuse_orgs_provider,use_orgs_live,use_org,OrgHandle(scope,stale,switcher).features/orgs/src/opener.rsscoped_opener: ⌘O across the scope; an item opens in the org it came from.features/orgs/src/server.rsconfine,current_org,in_org,Members,MembersResolver,ConfinedEngine,MEMBERS_TTL; the lane scope iscrates/architect/src/permissions_gate.rsLaneScopeandcrates/architect/src/host/mount.rsLane.
On the client, use_orgs_live fetches on every reconnect, and Refresh
either refreshes Orgs or keeps the cache and marks it stale. On the server,
a call on /org/acme/vox enters the lane’s org (the Lane scope, or
confine), which sets the current org for the call, then the gate, which
checks it there. MembersResolver resolves the principal and, at most
once per MEMBERS_TTL per token, asks the directory for that session’s orgs.
ConfinedEngine refuses a non-member of current_org() and any resource
under another org’s path, then applies the role’s rules inside the org.
Tests to read#
features/orgs/tests/orgs.rs: selection, capabilities by role with inheritance, per-org grants, leaving the current org, the JSON cache.features/orgs/tests/scope.rs: window scope against All, paths and document ids,read_allwith one org failing,Refreshper connection.features/orgs/tests/opener.rs(featurecommands): ⌘O follows the org.features/orgs/tests/server.rs(featureserver): a call runs in its lane’s org and the role there decides; another org’s resource is refused.features/orgs/tests/host_lane.rs(featureserver): on anEngineHostorg lane a member of another org is refused at the gate; an unscoped org lane fails closed underConfinedEngine::require_org.apps/auth-server/tests/surfaces.rs: orgs fetched live on every transport; a confining server learning the caller’s orgs from the identity server.
Status#
Done in architect, and already used by architect-files, the org sync driver,
settings (OrgSettings) and extensions (an org’s approved list). Not yet:
presence by org. Membership changes reach a server within MEMBERS_TTL.
A lane’s org alone refuses no one; the engine must be (or include)
ConfinedEngine. A gate with no All-organizations lane sets
ConfinedEngine::require_org, so a lane left unscoped is refused.
- Task keeps its own org selection (
task:crates/ui-core/src/orgs.rs); to adopt:architect-orgson the client andconfineon the server. - Studio does not use it yet.