Who may read, sign up and sign in to a space

Embed
Preview of the card
<a href="https://forumulate.com/guides/reader-access"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/22484eb1-829a-434f-bff9-0ea137fbf104/social-card.svg?v=3e7ae33eec8b" alt="Who may read, sign up and sign in to a space" width="600" height="315"></a>

A space's reader surface has three questions to answer about every reader: are they who they say they are, may they come in, and have they agreed to what you require of them? This guide covers the policy settings that answer the second and third, the moderator queue for reader accounts that need approval, and the public documents a custom front end reads to render the right sign-in step. How readers actually prove who they are is configured separately, in Reader sign-in methods and Sign readers in from your own app with a JWT.

Two policies, each with an org default and a space override

Both policies below work the same way. The organization default is an ordinary org setting (PUT /orgs/{orgId}/settings/{key}, reset with DELETE on the same address). A space may store an override on top of it. Resolution is space, then org, then the built-in default, and every field is resolved on its own.

Two rules apply to every override:

  • An override that states no field does not turn the policy off. It inherits the org default, exactly as having no override does. So PUT {} is harmless.

  • To remove an override, DELETE it. That removes the key rather than storing an empty record, and it is idempotent: deleting a space with no override answers 200, never 404.

Reads and writes require the space.update capability. The bodies are strict: an unknown key is a 422 rather than a 200 that stores nothing.

When a document is marked "members must accept it", the legal gate decides what a reader who has not accepted the current edition is kept from.

scope

What is withheld

none

Nothing. Acceptance is recorded when it happens. The default.

post

Posting, commenting, voting and applying. Reading stays open.

read

The space's content, until the reader accepts.

signin

The session itself: sign-in is not completed until the edition is agreed.

anonymous says what a read gate does about a reader it cannot identify: require_identity (the default) asks them to sign in, because an acceptance recorded against nobody is not evidence; cookie records it against a cookie-scoped pseudonymous subject, which stops re-asking the same browser and is marked verification: 'anonymous' in the register and the CSV export.

PUT /orgs/{orgId}/spaces/{spaceId}/legal-gate
{ "scope": "read", "anonymous": "require_identity" }

A read or signin gate on a space that offers no reader sign-in is refused with a named 422, because there would be nobody to record an acceptance against and no way through. GET answers the stored fields plus hasOverride and inherits; the org-level keys are legal.require_acceptance and legal.anonymous_acceptance. See Legal documents: binding and acceptance for the documents themselves.

How far one reader identity carries

By default a reader who signed in to one space is a stranger in the next, even on the same domain. Set scope to org and one identity carries across every space of the organization on that host.

PUT /orgs/{orgId}/spaces/{spaceId}/reader-identity
{ "scope": "org" }

The values are space (default) and org; the org-level key is reader.identity_scope. Identity is not admission: signing in somewhere does not admit a reader to a space whose own gate they have not passed. Readers already signed in per space are adopted rather than asked again.

Limiting a space to some networks

A third way to decide who may come in needs no sign-in at all: a network allow-list. settings.networkAccess.ipAllowList lists IP addresses or CIDR blocks (203.0.113.0/24), set with PUT /orgs/{orgId}/spaces/{spaceId}/network-access or in the studio under Settings → Network access. Empty, the default, means everyone. With a list, a reader from any other address sees a "Not available from this network" page, and the space's JSON, feeds, markdown twins and widgets answer 403 space.network_refused. The network is checked before a password or sign-in is asked for, and it applies to every space type except status pages, which keep their own list. See Limit a space to some networks.

Approving reader accounts

A new reader account waits in a queue until a moderator decides when the reader sign-in registry resolved for the space has approveNewMembers: true (on the organization's registry or the space's override), or when the organization setting users.must_approve_users is on. A reader whose email domain is listed in users.auto_approve_email_domains skips the queue that the organization setting creates, but never a registry's approveNewMembers. A pending reader still gets a session, so the reader surface can tell them they are awaiting approval. Listing, approving and rejecting need the forum.moderate capability.

GET /orgs/{orgId}/spaces/{spaceId}/reader-signups?status=pending
POST /orgs/{orgId}/spaces/{spaceId}/reader-signups/{signupId}/approve
POST /orgs/{orgId}/spaces/{spaceId}/reader-signups/{signupId}/reject

status is pending (the default), approved or rejected; any other value is a 422 rather than quietly showing you the pending list. Each row carries id, userId, email, name, status, source and createdAt, newest first, up to 500. Approving grants the reader a membership so they can post. Rejecting leaves them with read access but no posting.

What a custom front end reads

A reader surface has to know which sign-in methods to offer and which policy applies before it renders anything. Both documents are public, hold no secrets, and need no token:

GET /public/orgs/{orgId}/spaces/{spaceId}/reader-auth
GET /public/orgs/{orgId}/spaces/{spaceId}/reader-policy

reader-auth is the resolved, public-safe sign-in configuration (the same registry you write with PUT .../reader-auth, with every secret left out). reader-policy returns { legalGate, readerIdentity }, each field naming the level that decided it (space, org or default) so a client never has to infer it. Both are rate limited to 60 requests a minute per client.

The sign-in round trips

These are the public addresses a reader's browser passes through. None needs a token. Call them on the host the space is read on (<mount>/_api/... on a custom domain, /api/... on ours), because the cookies they set are host-only.

  • GET /public/orgs/{orgId}/spaces/{spaceId}/gate answers the space's gate mode (none, password, jwt or oidc) with spaceName, spaceId and spaceSlug, so one sign-in page of your own can serve every space.

  • GET /orgs/{orgId}/reader-auth/setup prints the two addresses a redirect sign-in needs. startUrl, which is GET /public/orgs/{orgId}/spaces/{spaceId}/visitor-oidc/start, sends the reader to the identity provider: ?method= picks a provider from the registry (google, microsoft, oidc and so on) and ?returnTo= is checked against the space's addresses, defaulting to the space's primary address. redirectUri, which is GET /public/visitor-oidc/callback, is the one callback you register at your provider for every space. The signed state carries the organization, the space and the return path; the callback sets the reader cookie (12 hours) and, when the reader gets an account, the session cookie, then redirects back.

  • A sign-in that returns to another host, such as your own domain, finishes in one more hop. The callback cannot set cookies there, so it hands the reader to GET /public/visitor-oidc/adopt under the space's mount with a ticket that is bound to that origin, lives 60 seconds and works once. An expired or replayed ticket sends the reader on without signing them in.

  • POST /public/orgs/{orgId}/spaces/{spaceId}/reader-auth/jwt redeems a JWT your own app minted, and GET /public/orgs/{orgId}/spaces/{spaceId}/reader-auth/jwt?jwt=… does the same from a link and redirects; see Sign readers in from your own app with a JWT.

Signing a reader out

POST /public/reader/sign-out signs a reader out on the host that answers. It revokes the session the reader sign-in created, and the Atlas session with it, and clears every reader cookie the request carries: forumulate_session, fm_signed_in and each cflx_vis_* pass. Call it same-origin, because a host-only cookie can only be cleared by its own host. The body is optional; send orgId and spaceId (and returnTo) and redirectTo in the answer is the space's signOutUrl carrying the return address, so one click also signs the reader out of your own app. A plain HTML form post is answered with a 303 to that address.

POST /auth/logout is narrower: it revokes the forumulate_session the request carries and clears that one cookie, answering { "ok": true }. Use the reader sign-out above for a reader, since logout leaves the reader passes in place.

In the API

Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.

Reader Auth Public

Operation

Request

What it does

Shortcuts

ReaderAuthPublic_resolved

GET /public/orgs/{orgId}/spaces/{spaceId}/reader-auth

Get the reader auth in a space

client.readers.publicAuth.resolved()

Reader Policy

Operation

Request

What it does

Shortcuts

ReaderPolicy_getLegalGate

GET /orgs/{orgId}/spaces/{spaceId}/legal-gate

One space's legal-acceptance gate override

client.readers.policy.getLegalGate()

ReaderPolicy_setLegalGate

PUT /orgs/{orgId}/spaces/{spaceId}/legal-gate

Replace one space's legal-acceptance gate override

client.readers.policy.setLegalGate()

ReaderPolicy_deleteLegalGate

DELETE /orgs/{orgId}/spaces/{spaceId}/legal-gate

Remove one space's legal-acceptance gate override

client.readers.policy.deleteLegalGate()

ReaderPolicy_getReaderIdentity

GET /orgs/{orgId}/spaces/{spaceId}/reader-identity

One space's reader-identity scope override

client.readers.policy.getReaderIdentity()

ReaderPolicy_setReaderIdentity

PUT /orgs/{orgId}/spaces/{spaceId}/reader-identity

Replace one space's reader-identity scope override

client.readers.policy.setReaderIdentity()

ReaderPolicy_deleteReaderIdentity

DELETE /orgs/{orgId}/spaces/{spaceId}/reader-identity

Remove one space's reader-identity scope override

client.readers.policy.deleteReaderIdentity()

Reader Policy Public

Operation

Request

What it does

Shortcuts

ReaderPolicyPublic_resolved

GET /public/orgs/{orgId}/spaces/{spaceId}/reader-policy

Get the reader policy in a space

client.readers.publicPolicy.resolved()

Reader Signup

Operation

Request

What it does

Shortcuts

ReaderSignup_list

GET /orgs/{orgId}/spaces/{spaceId}/reader-signups

List reader signups in a space

client.readers.signup.list()

ReaderSignup_approve

POST /orgs/{orgId}/spaces/{spaceId}/reader-signups/{id}/approve

Approve the reader signup

client.readers.signup.approve()

ReaderSignup_reject

POST /orgs/{orgId}/spaces/{spaceId}/reader-signups/{id}/reject

Reject the reader signup

client.readers.signup.reject()

Space Network Access

Operation

Request

What it does

Shortcuts

SpaceNetworkAccess_read

GET /orgs/{orgId}/spaces/{spaceId}/network-access

A space's network allow-list

client.spaces.networkAccess.read()

SpaceNetworkAccess_write

PUT /orgs/{orgId}/spaces/{spaceId}/network-access

Set a space's network allow-list

client.spaces.networkAccess.write()

The Forumulate teamUpdated