feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Active organization
|
||||
description: Set and switch the current organization context within your application.
|
||||
url: /docs/mobile/organizations/active-organization
|
||||
---
|
||||
|
||||
# Active organization
|
||||
|
||||
The active organization on mobile mirrors the behavior used on the [web app](/docs/web/organizations/active-organization) and in the [extension](/docs/extension/organizations). It is tracked in the authenticated session as `activeOrganizationId` and used to scope all organization-bound data and actions.
|
||||
|
||||
Below you can find how to read and work with the active organization in your mobile app context.
|
||||
|
||||
## Reading the active organization
|
||||
|
||||
Use your auth client's helper to read the active organization from the session. This keeps the client in sync with the server and avoids duplicating tenancy logic.
|
||||
|
||||
```tsx title="organizations.tsx"
|
||||
import { authClient } from "~/lib/auth";
|
||||
|
||||
export function OrganizationsScreen() {
|
||||
const organization = authClient.useActiveOrganization();
|
||||
const member = authClient.useActiveMember();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>{organization?.name}</Text>
|
||||
<Text>{member?.role}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This mirrors the [extension](/docs/extension/organizations) approach and the [web hook](/docs/web/organizations/active-organization), ensuring the active organization and member role stay consistent with the server session.
|
||||
|
||||
## Performing actions
|
||||
|
||||
When invoking API routes from the mobile app, prefer passing the `organizationId` explicitly with the payload. This guarantees the correct tenant is targeted even if multiple devices or views are active simultaneously.
|
||||
|
||||
```tsx title="create-post.tsx"
|
||||
import { api } from "~/lib/api";
|
||||
|
||||
export function CreatePost() {
|
||||
const activeOrganization = authClient.useActiveOrganization();
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: async (post: PostInput) =>
|
||||
api.posts.$post({
|
||||
...post,
|
||||
organizationId: activeOrganization?.id,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Button onPress={onSubmit(mutate)}>Submit</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This mirrors the recommendation from the [web guide](/docs/web/organizations/active-organization#api-route) and avoids edge cases tied to stale session values.
|
||||
|
||||
## Switching organizations
|
||||
|
||||
TurboStarter ships an account switcher out of the box for mobile. You can drop it into your app and customize labels and styling as needed.
|
||||
|
||||
```tsx title="settings.tsx"
|
||||
import { AccountSwitcher } from "~/modules/organization/account-switcher";
|
||||
|
||||
export function SettingsScreen() {
|
||||
return <AccountSwitcher />;
|
||||
}
|
||||
```
|
||||
|
||||
When a user selects a new organization, it calls your backend to update the session's `activeOrganizationId` and then re-read the session or invalidate related queries.
|
||||
|
||||
For deeper background on how the active organization is resolved, see the [web guide](/docs/web/organizations/active-organization).
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Invitations
|
||||
description: Send, track, and accept organization invites.
|
||||
url: /docs/mobile/organizations/invitations
|
||||
---
|
||||
|
||||
# Invitations
|
||||
|
||||
Invite teammates by email to join an organization directly from your mobile app. Acceptance is straightforward: we verify the invite, create or reuse the membership with the intended role, and set the user's active organization.
|
||||
|
||||
The implementation uses the same APIs and rules as the [web app](/docs/web/organizations/invitations) and is powered by the [Better Auth organization plugin](https://www.better-auth.com/docs/plugins/organization).
|
||||
|
||||

|
||||
|
||||
## Capabilities
|
||||
|
||||
* Send invitations by email.
|
||||
* View and filter invitations by status or role, and search by email.
|
||||
* Resend or revoke an invitation.
|
||||
* Accept an invitation via a [deep link](https://docs.expo.dev/linking/into-your-app/).
|
||||
|
||||
<Callout>
|
||||
Permissions are enforced by roles. Typically, only organization admins can
|
||||
send or manage invites. See [RBAC (Roles &
|
||||
Permissions)](/docs/mobile/organizations/rbac).
|
||||
</Callout>
|
||||
|
||||
## Inviting members
|
||||
|
||||
Sending an invitation typically requires the invitee's email and the intended role. You can add multiple recipients in the invitation form to invite several members at once.
|
||||
|
||||

|
||||
|
||||
After sending, the invitee receives an email with a link to accept. It's a [deep link](https://docs.expo.dev/guides/linking) that opens your app and automatically validates the invite.
|
||||
|
||||
## Handling invitations
|
||||
|
||||
When a recipient opens an invite link on their device, the app automatically handles the entire flow - reading, parsing, and validating the invite - for you.
|
||||
|
||||

|
||||
|
||||
When the user accepts, we create or reuse their membership and set the active organization in their session. If they reject the invite, we redirect them to their account home.
|
||||
|
||||
## Learn more
|
||||
|
||||
For underlying details shared across platforms, see the web documentation:
|
||||
|
||||
<Cards>
|
||||
<Card title="Data model" description="Schema for organizations and invitations" href="/docs/web/organizations/data-model" />
|
||||
|
||||
<Card title="Statuses and flow" description="Invitation status codes and how they update" href="/docs/web/organizations/invitations#status" />
|
||||
|
||||
<Card title="Automatic invalidation" description="How invitations are automatically cleaned up" href="/docs/web/organizations/invitations#automatic-invalidation" />
|
||||
|
||||
<Card title="Admin management" description="Admin tooling for managing invitations" href="/docs/web/organizations/invitations#invitation-management" />
|
||||
</Cards>
|
||||
|
||||
These cover the schema, token lifecycle, and admin tooling shared by the mobile and web apps.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Overview
|
||||
description: Learn how to use organizations/teams/multi-tenancy in TurboStarter mobile app.
|
||||
url: /docs/mobile/organizations/overview
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
Organizations let you build teams and multi-tenant SaaS out of the box in the mobile app.
|
||||
|
||||
Users can create organizations, invite teammates, assign roles, and seamlessly switch between workspaces — all from iOS/Android — with the same secure data isolation used on the [web app](/docs/web/organizations/overview).
|
||||
|
||||
<Callout title="What is multi-tenancy?">
|
||||
[Multi-tenancy](https://www.ibm.com/think/topics/multi-tenant) is a software architecture pattern where a single instance of an application serves multiple tenants, each with its own data and configuration.
|
||||
</Callout>
|
||||
|
||||
The feature is powered by the same [Better Auth organization plugin](https://www.better-auth.com/docs/plugins/organization) and shares TurboStarter's API, routing, and data layer with the [web app](/docs/web/organizations/overview) and [extension](/docs/extension/organizations). That means your mobile app benefits from the same tenancy rules, RBAC checks, and invitations flow without duplicating backend logic.
|
||||
|
||||
<ThemedImage light="/images/docs/web/organizations/multi-tenancy/light.png" dark="/images/docs/web/organizations/multi-tenancy/dark.png" alt="Architecture" width={1375} zoomable height={955} />
|
||||
|
||||
## Architecture
|
||||
|
||||
On mobile, TurboStarter uses the same pragmatic multi-tenant architecture as the [web app](/docs/web/organizations/overview):
|
||||
|
||||
* **Tenant context** lives in the authenticated session as the active organization ID. The mobile client reads this context from the API and includes it when making requests.
|
||||
* **Data scoping** is performed server-side via `organizationId` on tenant-owned tables and guard clauses in queries. Mobile screens consume scoped endpoints so users only see data for their selected organization.
|
||||
* **Authorization** combines tenant scoping with role checks. We separate “can access this tenant?” from “can perform this action within the tenant?”.
|
||||
* **Extensibility**: add new tenant-bound entities by including `organizationId` in your schema and using the provided helpers to read or switch the active organization in the app.
|
||||
|
||||
This keeps data isolated per organization while remaining simple to reason about across platforms.
|
||||
|
||||
<Callout>
|
||||
For deeper details on the shared data model used by the mobile app, see [Data
|
||||
model](/docs/web/organizations/data-model).
|
||||
</Callout>
|
||||
|
||||
## Concepts
|
||||
|
||||
The same core concepts apply in the mobile app:
|
||||
|
||||
| Concept | Description |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| **Organization** | A workspace that owns resources and settings, acting as an isolated tenant. |
|
||||
| **Member** | A user assigned to an organization. |
|
||||
| **Role** | Access level within an organization (see [RBAC](/docs/mobile/organizations/rbac)). |
|
||||
| **Invitation** | Email request to join an organization (see [Invitations](/docs/mobile/organizations/invitations)). |
|
||||
| **Active organization** | The currently selected organization in a user's session, used to scope data and permissions. |
|
||||
|
||||
These concepts provide the building blocks for flexible team management and secure, multi-tenant SaaS applications on mobile.
|
||||
|
||||
## Development data
|
||||
|
||||
In development, TurboStarter automatically [seeds](/docs/mobile/installation/commands#seeding-database) example data when you set up services. The mobile app connects to the same development API, so you can test the full organizations flow end-to-end:
|
||||
|
||||
* One organization is created by default.
|
||||
* All default roles are created and assigned within that organization.
|
||||
* Sample invitations are generated so you can test the invite flow.
|
||||
|
||||
You can safely experiment with these sample organizations, roles, and invitations to understand multi-tenancy features — [reset](/docs/mobile/installation/commands#resetting-database) or [reseed](/docs/mobile/installation/commands#seeding-database) anytime to return to the default state.
|
||||
|
||||
The default credentials for demo users can be customized using the `SEED_EMAIL` and `SEED_PASSWORD` environment variables.
|
||||
|
||||
<Callout type="error" title="Never run in production">
|
||||
The default development data and setup are intended for local development and
|
||||
testing only. **Never** use these seeds or configurations in a production
|
||||
environment - they are insecure and may expose sensitive functionality.
|
||||
</Callout>
|
||||
|
||||
## Customization
|
||||
|
||||
You have flexibility to adapt organizations to fit your mobile experience. For example, you might rename labels (such as Organization to *Team* or *Workspace*), and update the app copy accordingly.
|
||||
|
||||
You can adjust the available [roles and permissions](/docs/mobile/organizations/rbac) to suit your access model.
|
||||
|
||||
The [invitation flow](/docs/mobile/organizations/invitations) can be customized, including how verification, onboarding, or metadata capture work.
|
||||
|
||||
Feel free to check how to configure all of these features inside mobile application in the dedicated sections linked above.
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: RBAC (Roles & Permissions)
|
||||
description: Manage roles, permissions, and access scopes.
|
||||
url: /docs/mobile/organizations/rbac
|
||||
---
|
||||
|
||||
# RBAC (Roles & Permissions)
|
||||
|
||||
Role-based access control (RBAC) lets you define who can do what in an organization.
|
||||
|
||||
<Callout title="New to RBAC?">
|
||||
If you're new to the RBAC concept, a simple mental model is:
|
||||
|
||||
* Users belong to organizations.
|
||||
* Users get roles.
|
||||
* Roles map to permissions on resources.
|
||||
</Callout>
|
||||
|
||||
In TurboStarter, we primarily rely on the [Better Auth plugin](https://www.better-auth.com/docs/plugins/organization) for the heavy lifting—roles, permissions, teams, and member management—while handling critical logic with our own code.
|
||||
|
||||
This provides a flexible access control system, letting you control user access based on their role in the organization. You can also define custom permissions per role.
|
||||
|
||||
<Callout title="Everything is configured out of the box!">
|
||||
TurboStarter ships with the default RBAC system configured out of the box. This setup may be enough if you're not planning a very complex access control system, but you can also easily customize it to your needs.
|
||||
|
||||
On mobile, use conditional UI (disable or hide actions) together with client helpers to match each member's role.
|
||||
</Callout>
|
||||
|
||||
## Roles
|
||||
|
||||
Roles are named bundles of permissions. Keep them few and well-defined. By default, we have the following roles:
|
||||
|
||||
```ts
|
||||
const MemberRole = {
|
||||
MEMBER: "member",
|
||||
ADMIN: "admin",
|
||||
OWNER: "owner",
|
||||
} as const;
|
||||
```
|
||||
|
||||
A user can have multiple roles in an organization. For example, a user can be a member and an admin (if it makes sense for your application).
|
||||
|
||||
<Callout type="warn" title="Don't confuse organization admin with super admin">
|
||||
The organization's `admin` role is different from the user's global `admin` role.
|
||||
|
||||
The organization `admin` governs permissions only inside the organization, whereas the global `admin` controls access to the [super admin dashboard](/docs/web/admin/overview).
|
||||
</Callout>
|
||||
|
||||
To create additional roles with custom permissions, see the [official documentation](https://www.better-auth.com/docs/plugins/organization#create-access-control) for more details.
|
||||
|
||||
## Permissions
|
||||
|
||||
Permissions represent what actions a role can perform on which resources.
|
||||
|
||||
To check if the current user has permission to perform an action on mobile, use the client helper and handle the boolean result in your component logic.
|
||||
|
||||
```tsx title="create-project.tsx"
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { authClient } from "~/lib/auth";
|
||||
|
||||
export function CreateProject() {
|
||||
const { data: canCreate } = useQuery({
|
||||
queryKey: ["permission", "project", "create"],
|
||||
queryFn: () =>
|
||||
authClient.organization.hasPermission({
|
||||
permissions: { project: ["create"] },
|
||||
}),
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: async () => {
|
||||
// perform the create action
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={canCreate === false}
|
||||
loading={isPending}
|
||||
onPress={() => canCreate && mutate()}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
When you already have the active member's role, prefer the client-side `checkRolePermission` to avoid extra API calls.
|
||||
|
||||
```tsx title="update-project.tsx"
|
||||
import { authClient } from "~/lib/auth";
|
||||
|
||||
export function UpdateProject() {
|
||||
const activeMember = authClient.useActiveMember();
|
||||
|
||||
const canUpdate = authClient.organization.checkRolePermission({
|
||||
permission: {
|
||||
project: ["update"],
|
||||
},
|
||||
role: activeMember.role,
|
||||
});
|
||||
|
||||
return <Button disabled={!canUpdate}>Update</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
We leverage the existing hook to retrieve the active member role within the [active organization](/docs/mobile/organizations/active-organization) context. That way, you can easily check whether a member has permission to perform an action without a server round trip.
|
||||
|
||||
<Callout type="warn">
|
||||
This does not include any dynamic roles or permissions because everything runs synchronously on the client-side. Use the `hasPermission` APIs to include checks for dynamic roles and permissions.
|
||||
</Callout>
|
||||
|
||||
If you need to add more granular permissions to existing roles, or create new ones, use the [`createAccessControl`](https://www.better-auth.com/docs/plugins/organization#custom-permissions) API.
|
||||
|
||||
For further customization—such as dynamic access control, lifecycle hooks, or team management—see the guidance in the [official documentation](https://www.better-auth.com/docs/plugins/organization) and the [web guide](/docs/web/organizations/rbac).
|
||||
Reference in New Issue
Block a user