SaaS backend for teams

Multi-user projects with roles, permissions, groups and row-level security, so each team only ever sees its own records.

Last updated

A SaaS product has one codebase and many customers, and each customer has a team of people who must never see another team's records. Anythink gives you the pieces for that without a backend to write: sign-in and registration, roles, groups, row-level security, API keys and separate projects for each environment.

This page is for developers building a product where customers belong to teams or organisations. It walks through one example end to end. Each step shows the Anythink dashboard, the CLI and an AI assistant (MCP), and links to the guide that covers the detail.

If you haven't used Anythink before, read Core concepts first.

What you'll build#

  • A teams entity for your customer organisations, and a projects entity that each team owns. projects has row-level security switched on.
  • Sign-up for your app's users, with a default role of Team member.
  • Two roles: Team admin and Team member.
  • A group for each team, so a record can be shared with the whole team and team admins can read everything the team's members have created.
  • An API key for a server-side integration.
  • Separate development, staging and production projects, with your setup copied between them.

There's no built-in "organisation" object. A team is two things you create: a record in teams, and a group with the same name. The record carries your own data, such as a plan name or a billing contact. The group is what row-level security uses.

Step 1: Model teams and the records they own#

Create teams first, then projects, and turn on row-level security for projects before any records exist. Records created while it was off have no grants, so they're hidden from everyone except administrators once you switch it on.

Entity Field Type Notes
teams name Small text Required. The name field.
teams group_id Integer The id of the team's group. Filled in at step 4.
projects title Small text Required. The name field.
projects status Small text, select display Options active and archived.
projects team Many-to-One Points at teams.
projects team_group_id Integer The team's group id, set when the record is created. Used to filter and report by team.

In the Anythink dashboard

  1. Go to Settings › Data Model, select Add Entity, name it teams and select Create Entity. Add the name and group_id fields.
  2. Add another entity called projects. Tick Enable Row-Level Security, then select Create Entity.
  3. Add the projects fields from the table. For team, choose Many-to-One and pick teams as the target.

With the CLI

bash
anythink entities create teams
anythink fields add teams name --type varchar --required --name-field
anythink fields add teams group_id --type integer

anythink entities create projects --rls
anythink fields add projects title --type varchar --required --name-field
anythink fields add projects status --type varchar --display select --options "active,archived" --default active
anythink fields add projects team --type many-to-one --target teams
anythink fields add projects team_group_id --type integer

With an AI assistant (MCP)

text
Create two entities. teams has a required name field that is the name field, and an integer group_id. projects has row-level security on, a required title that is the name field, a select field called status with active and archived and a default of active, a many-to-one field called team that points at teams, and an integer team_group_id.

The assistant runs entities create and one fields add per field through the cli tool. Connect Claude to your backend with MCP sets up the MCP server. For every field type, see Model your data. To switch row-level security on later, or to see what it filters, see Groups and row-level security.

Step 2: Sign people in, with a default role#

Your app signs users in with email and password, or with social sign-in. Anythink returns a short-lived access token and a longer-lived refresh token, and applies the user's role and row-level security on every request. Registration is controlled by three project settings: Allow registrations, Require email confirmation for new users and Default role for new users. Everyone who registers themselves gets the default role, so set it to the least powerful role you have.

Create the Team member role first (step 3), then come back and choose it here.

In the Anythink dashboard

  1. Open Settings › Users.
  2. In the User access card, tick Allow registrations.
  3. Choose Team member as the Default role for new users. Administrator roles aren't offered.
  4. Tick Require email confirmation for new users if people must confirm their address before they sign in, then select Save.
  5. Open Settings › Organisation Settings and add your app's address under Allowed Application URLs. Anythink domains and localhost always work.

With the CLI

These project settings are managed in the dashboard. Use the steps above. Once they're set, registration is an API call, and the CLI can make it for a test:

bash
anythink fetch /auth/v1/register --method POST --body '{
  "first_name": "Alice",
  "last_name": "Smith",
  "email": "alice@example.com",
  "password": "Str0ng!passphrase"
}'

To add someone yourself with a role, invite them. They set their own password from the emailed link:

bash
anythink users invite bob@example.com Bob Jones --role-id 104

With an AI assistant (MCP)

These project settings are managed in the dashboard, and your assistant can walk you through them. Ask it to invite a user instead:

text
Invite bob@example.com, Bob Jones, as a Team member.

The assistant runs roles list to find the role, then invites the user. Don't paste real passwords into a chat.

In your app, sign in with POST /org/{project_id}/auth/v1/token and send the access token as Authorization: Bearer …. React apps can use the SDK, which stores the session and refreshes it for you. Sign in users covers confirmation, sessions and password resets, and Anythink SDK: sign-in and sessions covers the React side. Manage app users and team members covers inviting, editing and deleting users.

Step 3: Create roles for team admins and members#

A role decides what a user can do anywhere in the project. Each user has exactly one. Create a role for each job and tick only the permissions it needs.

Role projects Other
Team member read, create, update teams:read
Team admin read, create, update, delete teams:read, teams:update, and anythink_group_data:read

Both roles need API Access ticked, or every call to your entities returns 403. The Team admin role's anythink_group_data:read is what step 4 uses. It's the permission that lets someone read all the records created by the members of a group they belong to.

Don't use Standard User for either. It's meant for your own team and can change entities and secrets.

In the Anythink dashboard

  1. Open Settings › Users and select the Roles & Permissions tab.
  2. Select +, enter Team member, tick API Access and select Create Role. Repeat for Team admin.
  3. Select each role. Under Data Model Permissions, tick the actions from the table. For Team admin, find anythink_group_data under Anythink Permissions and tick Read.
  4. Select Update Role.

With the CLI

bash
anythink roles create "Team member" --description "A person on a customer team"
anythink roles create "Team admin" --description "Runs a customer team"
anythink roles list

Note: Role permissions are managed in the dashboard. Use the steps above, including to tick API Access. The CLI can then show you what a role holds.

bash
anythink roles permissions list 104

With an AI assistant (MCP)

Note: Role permissions are managed in the dashboard. Your assistant can create the roles and show what they hold, and walk you through ticking the permissions.

text
Create two roles, Team member and Team admin, then show me the permissions on each.

To move a user to a different role later, open them under Settings › Users and change their Role. The change applies on their next request. Roles and permissions covers the settings, field-level access and how to check what a user can do, and Permissions reference lists every permission.

Step 4: Give each team a group, and scope records to it#

A group is a named set of users. Share a record with a group once and every current member can reach it. Remove someone from the group and they lose that access straight away.

Group grants are off until you turn them on for the project. Then, for each customer team:

  1. Create a group with the team's name.
  2. Add the team's members. Each membership has a role. Give team admins the Team admin role and everyone else Team member.
  3. Store the group's id on the team's record, so you can find it from your app.
  4. Grant records to the group.

In the Anythink dashboard

  1. Open Settings › Organisation Settings. Under Access Controls, tick Enable group-level row access and select Save Changes.
  2. Open Settings › Groups and select Create Group. Name it after the team, for example Acme.
  3. Open the group, select Add member under Members, choose the user and a role, and select Add member. Members whose role can read the group's data show a Staff badge.
  4. Open the team's record in teams and set group_id to the group's id.
  5. To share a projects record with the team, open it, choose the Row-Level Security tab, select Add Group Access under Groups, choose the group and select Add Group. Untick Read Only Access to let members edit it.

With the CLI

There's no groups command yet, so call the groups API with fetch:

bash
anythink fetch /groups --method POST --body '{"name":"Acme","description":"Acme Ltd"}'
anythink fetch /groups/4/members --method POST --body '{"user_id":72,"role_id":104}'
anythink fetch /entities/projects/items/42/rls-groups --method PUT --body '{"group_id":4,"readonly":false}'

The group's id comes back from the first call, and anythink roles list shows the role ids. Turn on Enable group-level row access in the dashboard first: there's no CLI command for it.

With an AI assistant (MCP)

text
Create a group called Acme, add users 72 and 73 to it with the Team member role, and share project 42 with the Acme group so members can edit it.

The assistant finds the role with roles list, then calls the groups endpoints through fetch. Turning on group row access is a dashboard setting.

What each person then sees:

  • Members see the records they created, because the creator always gets read-write access to a record, and every record granted to their group. They never see another team's records: those return 404.
  • Team admins can also read everything their team's members have created, with a group-scoped read. They send group_scope on the normal list endpoint, using their own access token:
http
GET /org/{project_id}/entities/projects/items?group_scope=4
Authorization: Bearer <team admin's access token>

The caller must be a member of group 4 whose membership role holds anythink_group_data:read. Anyone else gets 403. A group-scoped read is read-only, and peers don't see each other's records unless a record is granted to the group.

To stamp the team onto records and share them with it automatically, use a workflow. A Create data step can resolve the groups a user belongs to on the server, grant the new record to them and write the group id into team_group_id. Groups and row-level security has the step settings, a worked example and the details of group_scope.

Note: Row-level security filters what users can read and change. Workflows and administrators bypass it, so filter explicitly in a workflow step and test with a Team member account, not an administrator.

Step 5: Use API keys for server-side integrations#

Your app's users call the API with their own access token, so their role and row-level security apply. Code you run yourself, such as a nightly job, a billing sync or a service that onboards new teams, has nobody signed in. It uses an API key, sent in the x-api-key header.

A key carries the permissions you tick when you create it, and acts as the user who created it for row-level security. A key created by an administrator reads every record. That's right for a trusted server and wrong for anything else, so create one key per job with the smallest set of permissions that works.

For example, a key for a billing sync that reads teams and their projects:

In the Anythink dashboard

  1. Open My Account and select the API Keys tab.
  2. Enter a Name such as billing-sync and choose Expires in (days).
  3. Under Permissions, tick teams:read and projects:read.
  4. Select Create API Key, and copy the key from the banner. It's shown once.

With the CLI

bash
anythink api-keys create billing-sync \
  --permissions teams:read,projects:read \
  --expires-in 90 \
  --save-as billing-sync \
  --yes

With --save-as, the key goes into a new CLI profile and is never printed. Use it with anythink --profile billing-sync <command>.

With an AI assistant (MCP)

text
Create an API key called billing-sync with teams:read and projects:read, expiring in 90 days, and save it as the profile billing-sync.

The assistant runs api-keys create through the cli tool. Ask for --save-as so the key doesn't pass through the conversation.

Call the API from your server like this:

bash
curl "https://api.my.anythink.cloud/org/$PROJECT_ID/entities/projects/items?page=1&pageSize=50" \
  -H "x-api-key: $ANYTHINK_API_KEY"

Keep keys on the server. Never put one in a browser or mobile app. A key can also add people to groups, if an administrator creates it with anythink_groups:read and anythink_groups:update. That's how a signup service on your server can put new users in the right team. Those permissions apply to every group in the project, so keep them on server-side keys and off app roles. See API keys for rotation, revoking and least privilege.

Step 6: Run development, staging and production as separate projects#

Anythink has no environment switch. An environment is a project. Each has its own data, files, users, roles, API keys, secrets and plan, so a mistake in development can't touch production. Name them in one pattern, such as acme-dev, acme-staging and acme-prod, and copy your setup forward with anythink migrate.

In the Anythink dashboard

  1. Open My Anythink and go to your billing account.
  2. Select Create Project, choose a plan and name it, for example acme-staging. Repeat for each environment.
  3. Open each project from the portal. Check the project name in the header before you change anything.
  4. In every project, set Allow registrations, Default role for new users and Allowed Application URLs separately. They aren't shared.

The dashboard has no migrate screen. To copy a data model by hand, create the same entities and fields in the target.

With the CLI

bash
anythink projects create "acme-dev" --region lon1
anythink projects create "acme-staging" --region lon1
anythink projects use acme-dev
anythink projects use acme-staging

anythink migrate --from acme-dev --to acme-staging --include-roles --include-menus --dry-run
anythink migrate --from acme-dev --to acme-staging --include-roles --include-menus

--dry-run prints what would be created and changes nothing. Run it first, and name the profile with --profile on any command that changes data, so you don't land on the wrong project.

migrate copies entities, fields, relationships and, with the flags above, roles with their permissions and menus. It only adds: anything that already exists on the target is skipped. It doesn't copy users, groups, API keys, secrets or settings, so create those in each project. Use --include-settings only on a project whose settings you're happy to overwrite.

With an AI assistant (MCP)

text
Preview a migration from acme-dev to acme-staging that includes roles and menus. Show me the summary and don't change anything.

The local anythink-mcp server runs migrate through its cli tool. Review the dry-run summary before you ask for the real run.

Before you go live, work through the checklist in Environments and migrating data. Check each role's permissions in production: a missing permission is the most common cause of a 403 after a promotion. Then sign in as a real Team member and a Team admin, and confirm each sees only what they should.

What you get, and the limits#

What you get

  • Sign-in and registration with email and password, social sign-in and a React SDK, with a default role for new users.
  • Roles that decide what each person can do, with permissions per entity and per field.
  • Row-level security on any entity, with grants to individual users or to groups.
  • Groups that you can add people to and remove them from, with group-scoped reads for team admins.
  • Scoped API keys for your own servers, and a separate project for each environment.

Limits to plan around

  • No built-in organisation object. A team is a record you model and a group you create. Anythink doesn't link them for you, so store the group id on the team's record.
  • One role per user, per project. Roles don't stack. Build a role that holds the mix you need. A user can belong to several groups.
  • Group-scoped reads are read-only. A team admin can read the team's records with group_scope. Editing a record still needs a grant on it, or an administrator.
  • Grants on existing records are administrator-only. App users can't change who a record is shared with. Do it in the dashboard, or in a workflow.
  • Group membership is managed by permission, across the project. The anythink_groups permissions aren't tied to one group. Give them to administrators and server-side keys, not to app roles.
  • Administrators and workflows bypass row-level security. An API key acts as the user who created it. Don't create keys as an administrator for anything that should see only one team's data.
  • Turn row-level security on before you write data. Records created while it was off are hidden from everyone but administrators until you grant access.
  • Role permissions are dashboard-only. The CLI and MCP can create roles and read what they hold.
  • Token lifetimes are fixed. Access tokens last 30 minutes and refresh tokens 30 days. You can't change them per project.
  • Users are limited by plan. The number of monthly active users is set by your plan.
  • Environments are separate. migrate only adds, and doesn't copy users, groups, secrets or API keys.

Next steps#