SnappSnapp
  • Getting Started
  • Installation
  • Configuration
  • Styling
  • Introduction
  • Custom URLs
  • Authentication
  • Multi-Domain Architecture
  • Team Management
  • Third-Party Integrations
  • Metrics & Analytics
  • API Reference
  • English
  • Italiano
  • Getting Started
  • Installation
  • Configuration
  • Styling
  • Introduction
  • Custom URLs
  • Authentication
  • Multi-Domain Architecture
  • Team Management
  • Third-Party Integrations
  • Metrics & Analytics
  • API Reference
  • English
  • Italiano
  • Features

    • Introduction
    • Custom URLs
    • Authentication
    • Multi-Domain Architecture
    • Team Management
    • Third-Party Integrations
    • Metrics & Analytics
    • API Reference

Overview

Teams are the unit Snapp uses to scope access inside an organization.

A team is created under an organization and then referenced by:

  • membership (who is part of the team)
  • policies (what each role can do within that team context)
  • resource bindings (which URLs are shared with that team)

Snapp uses Better-Auth organizations + access-control statements to enforce all of this.


Concepts

Organization

An organization is the boundary for identity and permissions. In Snapp, organizations are created and selected per host domain.

Teams only exist inside an organization.


Team

A team is a container that can be referenced as a permission context.

In your policy model, the context key is the team id:

permissions: {
  [teamId]: ['create', 'read', 'update', 'delete']
}

This allows per-team rules without hardcoding team names in code.


Roles

Snapp uses organization roles:

  • owner
  • admin
  • member

The role defines the default organization-level capabilities and is also used as the axis for team policies.


Permission model

Statements

Snapp defines a small set of actions:

  • create
  • read
  • update
  • delete
  • cancel

A policy is stored as:

type TPermissions = Record<string, ('create' | 'read' | 'update' | 'delete' | 'cancel')[]>

And the persisted structure is a role → teamId → actions map:

Record<Role, Record<TeamId, Action[]>>

This structure is stored in the database in organizationRole.permission.


Where policies live

Policies are stored per organization in the organizationRole table:

  • organizationId (the org)
  • role (owner | admin | member)
  • permission (JSON blob)

At runtime, Snapp loads policies for the current organization and builds the effective role graph.


Creating teams

UI flow

Teams are created from the organization panel.

The creation form collects:

  • team name
  • initial permissions per role (matrix editor)

The submitted payload includes:

  • organizationId
  • name
  • permissions (JSON string)

Server flow

Team creation does three things:

  1. Authorization check

    • The caller must have permission to create teams:
    permissions: { team: ['create'] }
    
  2. Create the team in Better-Auth.

  3. Materialize the policy

    • For each stored role in organizationRole, Snapp updates the permission JSON by adding:
    permission[team.id] = permissions[roleName]
    

After creation, caches are invalidated:

  • auth client cache (per host)
  • role cache (per organization)

This ensures any subsequent request sees the updated policy graph.


Editing team policies

Policy editor

The UI shows a permission matrix:

  • rows = roles (owner, admin, member)
  • columns = actions (create, read, update, delete)
  • target = a specific team id

Toggling a checkbox updates the local permissions map and triggers a save.

owner is locked to full access in the UI.


Save operation

When saving, Snapp sends a batch update:

[{ id: roleRowId, p: permissionsForThatRole }]

Server-side, this updates organizationRole.permission for each role row.

After update:

  • the role cache is invalidated
  • the auth client cache is invalidated

So the next permission check uses the new policy graph.


How enforcement works

Permission checks are server-side

Snapp does not trust the UI. The UI only controls convenience; enforcement happens on the server through hasPermission.


Permission checks use a context key

When a resource is shared with a team, the team id becomes the permission context.

Examples from URL operations:

  • Creating a URL with team bindings requires the caller to have create on at least one selected team.
  • Updating or deleting a URL owned by someone else requires the caller to have the matching permission for the teams that URL is assigned to.

This is enforced by evaluating:

hasPermission({
  organizationId,
  context: teamId,
  permissions: ['create' | 'update' | 'delete']
})

Create URL enforcement

When a URL is created with teams:

  • Snapp checks the caller against every selected team
  • required action: create
  • the operation is allowed if at least one team check succeeds

If none succeed, the request is rejected.


Update URL enforcement

When updating a URL:

  • if caller is owner or admin → allowed
  • otherwise, Snapp computes the union of:
    • teams already assigned to the URL
    • teams requested in the update

Then checks permission:

  • required action: update
  • allowed if any team check succeeds

This supports incremental changes without letting non-members bypass policy by removing teams.


Delete URL enforcement

When deleting URLs:

  • owners and admins can delete their own / all (depending on role logic)
  • non-owners can delete only if all team checks pass for delete

This yields a stricter rule for destructive operations.


Team assignment to resources

Snapp links teams to URLs through a join table (teamToUrl).

This has two purposes:

  • sharing: team members can operate on shared URLs if policy allows
  • enforcement context: team id becomes the policy key for authorization

The join table is updated during URL create/update inside the same DB transaction as the URL mutation.


Cache behavior

Two caches are involved:

  • auth instance cache (authCache.auth) keyed by host origin
  • role cache (authCache.roles) keyed by organization id (slugify(host.origin))

Any operation that mutates teams or policies clears the relevant cache keys so permission checks remain current.


Summary

  • Teams are permission contexts inside an organization.
  • Policies are stored as role → teamId → actions in organizationRole.permission.
  • The UI edits policies, but enforcement happens server-side using hasPermission.
  • URL operations use team assignments to decide which policies apply.
  • Cache invalidation keeps the effective permission graph consistent across requests.
Prev
Multi-Domain Architecture
Next
Third-Party Integrations