Security
OneStarter is built with security in mind. This means:
- Session data stored and accessed in secure server-side cookies
- Strong access control RBAC system
- Secure multi-tenancy system through RLS
- Choosing trusted infrastructure providers where sensitive data is encrypted at rest
Session Management
Sessions are stored via the backend in cookies. They are only ever accessed by the backend. Session data matches
the Supabase cookie session data. These cookies are signed to prevent
tampering in the backend. You can set your COOKIE_SIGNING_SECRET
in the .dev.vars
when developing locally, or as
Github Actions secrets for deployment. Treat this value as you would a password.
By default, Supabase stores sessions in client side cookies. Many pentests will flag this as a security hole. As a result, OneStarter patches this through server-side cookies only. These cookies are then exchanged for session data which is used for client-side Supabase API requests. At no point is session data stored in client cookies or LocalStorage/SessionStorage.
User-Tenant Context
Each user of the application can belong to multiple tenants by default.
As a result, it is important that your application is designed in such a way that the appropriate tenant context is ( almost) always accessible.
Should you only require a single tenant per user, we would strongly recommend adjusting this only from the client-side, and leaving the backend to support multi-tenancy should there be a need to expand.
Multi-tenancy is one of those features that is significantly easier to bake in from the beginning versus later on in development.
The tenant context is determined in the backend via routing. API endpoints that do tenant-level operations contain a
:tenantId
path parameter.
In the frontend, the tenant context belongs to the organisation slug, typically in the form of the route
/org/:orgSlug
. The slug is used to fetch the correct tenant context.
The API endpoint /api/v1/context/:tenantId
is used to fetch a user-tenant context containing:
- Tenant information
- Features available to the tenant
- Billing plan
- User information for that tenant
- Granular permissions
There are a few scenarios where you do not need tenant context accessible:
- User-level management (profile, password reset, MFA verification, etc.)
- Admin control panel
Permissions System
User permissions are stored in the database using a role-based access control system.
This means that each user has roles
they assume. Each role belongs to a tenant. Each role has many role_permissions
.
Permissions are application wide and defined in your migration. There are some default roles out of the box, you can adjust which of these are relevant for you.
At the end there is a view granular_permissions
which provides an overview of permissions by user by tenant.
Adding new permissions
There are utility functions available to you in Postgres for handling permissions.
-- after defining your table, set the RLS tenant-based restrictions using helper functions
call private.add_rls_tenant_permission_policy('api', 'mytable', 'select');
call private.add_rls_tenant_permission_policy('api', 'mytable', 'insert');
call private.add_rls_tenant_permission_policy('api', 'mytable', 'update');
-- add the permissions globally so they can be used by roles
insert into api.permissions (type, object, default_on)
values ('select', 'mytable', array ['owner', 'admin', 'member']),
('insert', 'mytable', array ['owner', 'admin', 'member']),
('update', 'mytable', array ['owner', 'admin', 'member'])
on conflict (type, object) do update set default_on = excluded.default_on;
-- this will sync permissions with the default roles built into the system for existing users
select private.sync_default_permissions();
Default Roles
Default roles are used to cascade permissions down to existing users/roles. You can adjust these as you see fit. The initial migration runs this query to create the first three roles
insert into api.default_roles (name, is_owner)
values ('owner', true),
('admin', false),
('member', false);
You should only have one default role that is an owner