Team-Centric Architecture Guide¶
Version: 1.0 Last Updated: 2026-01-09 Purpose: Comprehensive guide to Vellocity's team-centric multi-tenancy architecture
Overview¶
Vellocity uses a team-centric multi-tenancy model where Team is the primary tenant, not User. This architecture enables:
- Multi-team collaboration: Users can belong to multiple teams
- Shared resources: Companies, products, and credits are team-owned
- Data isolation: Clear boundaries between teams
- Flexible permissions: Role-based access within teams
Architecture Diagram¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ TEAM-CENTRIC DATA MODEL │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ USER │ │
│ │ │ │
│ │ - active_team_id│ ← Current team context │
│ │ - email │ │
│ │ - aws_creds │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────────────────────┴─────────────────────┐ │
│ │ teams() (Many-to-Many via TeamMember) │ │
│ │ myCreatedTeam() (One owns, many joins) │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ TEAM_MEMBER │ │ TEAM │ │
│ │ (Pivot) │ │ (Tenant) │ │
│ │ │ │ │ │
│ │ - team_id │◄───────────────────────│ - user_id │ (owner) │
│ │ - user_id │ │ - entity_credits│ │
│ │ - role │ │ - word_credit │ │
│ │ - status │ │ - allow_seats │ │
│ │ - joined_at │ │ │ │
│ │ - remaining_* │ └────────┬────────┘ │
│ └─────────────────┘ │ │
│ │ │
│ ┌──────────────────────────────────────────┼──────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌────────────┐ ┌─────────┐ │
│ │ COMPANY │ │ PARTNER │ │ etc. │ │
│ │ (Brand) │ │ (Co-Sell) │ │ │ │
│ │ │ │ │ │ │ │
│ │ - team_id (FK) │ │ - team_id │ │ │ │
│ │ - user_id (FK) │ │ │ │ │ │
│ └────────┬────────┘ └────────────┘ └─────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ PRODUCT │ │
│ │ (Listing) │ │
│ │ │ │
│ │ - team_id (FK) │ ← Inherits from company │
│ │ - company_id │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Key Concepts¶
1. Team as Primary Tenant¶
Unlike user-centric models, Vellocity treats Team as the primary organizational unit:
| Aspect | User-Centric | Team-Centric (Vellocity) |
|---|---|---|
| Resource ownership | User owns resources | Team owns resources |
| Collaboration | Share via permissions | Native multi-user access |
| Subscriptions | Per-user billing | Team-level billing |
| Data isolation | Per-user | Per-team |
2. User-Team Relationships¶
Users interact with teams in three ways:
// 1. Team the user CREATED (ownership)
$user->myCreatedTeam(); // HasOne - Team where teams.user_id = users.id
// 2. Teams the user JOINED (membership)
$user->teams(); // BelongsToMany via team_members
// 3. Currently ACTIVE team (context)
$user->activeTeam(); // Based on active_team_id field
3. Multi-Team Membership¶
Users can belong to multiple teams simultaneously:
// User joins multiple teams
TeamMember::create(['team_id' => 1, 'user_id' => $user->id, 'role' => 'admin']);
TeamMember::create(['team_id' => 2, 'user_id' => $user->id, 'role' => 'member']);
// User's active team determines current context
$user->setActiveTeam($team2);
// Queries automatically scope to active team
Company::forTeam()->get(); // Returns team 2's companies
Database Schema¶
Users Table¶
users
├── id
├── email
├── team_id -- Legacy: user's "default" team (deprecated)
├── active_team_id -- NEW: Currently selected team context
├── team_manager_id
├── training_data_opt_out
├── analytics_opt_in
└── ...
Teams Table¶
teams
├── id
├── user_id -- Team owner/creator (FK to users)
├── name
├── is_shared
├── allow_seats -- Max team members
├── word_credit -- Team credit pool
├── entity_credits -- JSON: per-model credits
├── training_data_opt_out
├── analytics_opt_in
└── ...
Team Members Table¶
team_members
├── id
├── team_id -- FK to teams
├── user_id -- FK to users (nullable for invites)
├── email -- For pending invitations
├── role -- owner|admin|member|viewer
├── status -- waiting|joined|declined
├── joined_at
├── allow_unlimited_credits
├── remaining_words -- Per-member limit (optional)
├── remaining_images -- Per-member limit (optional)
└── ...
Companies Table¶
companies
├── id
├── user_id -- Creator (legacy, kept for migration)
├── team_id -- NEW: Team owner (primary ownership)
├── name
├── team_access -- JSON: shared access to other teams
└── ...
Team Member Roles¶
| Role | Description | Permissions |
|---|---|---|
owner |
Team creator | Full control, billing, delete team |
admin |
Team administrator | Manage members, all resources |
member |
Standard member | Create/edit own, view team resources |
viewer |
Read-only | View team resources only |
api_service |
Service account | API access only, no console login |
Note: For detailed capability-based permissions, see Team Member Capabilities.
BelongsToTeam Trait¶
Models that are team-owned use the BelongsToTeam trait:
Provided Methods¶
// Relationships
$company->team(); // BelongsTo Team
// Query Scopes
Company::forTeam($teamId)->get(); // Filter by team
Company::accessibleByUser($user)->get(); // User's accessible
Company::accessibleByAnyTeam($user)->get(); // All user's teams
// Access Control
$company->teamHasAccess($teamId); // Check access
$company->getAccessibleTeamIds(); // List teams with access
$company->grantTeamAccess($teamId, 'read'); // Share with team
$company->revokeTeamAccess($teamId); // Remove sharing
Auto-Assignment¶
When creating resources, team_id is automatically set from the user's active team:
// BelongsToTeam::bootBelongsToTeam()
static::creating(function ($model) {
if (empty($model->team_id) && auth()->check()) {
$model->team_id = auth()->user()->activeTeam()?->id;
}
});
Credit & Subscription Model¶
Team-Level Credits¶
Credits are allocated and tracked at the Team level:
SUBSCRIPTION (Plan)
│
▼
┌─────────────────────────────────────────────────────┐
│ TEAM │
│ │
│ word_credit: 500,000 ← Shared pool │
│ entity_credits: { │
│ "bedrock-claude-haiku": 50000, │
│ "bedrock-nova-lite": 100000 │
│ } │
│ seo_credits_remaining: 100 │
│ │
└─────────────────────────────────────────────────────┘
│
▼ Members consume from pool
┌─────────────────┐ ┌─────────────────┐
│ Team Member A │ │ Team Member B │
│ (unlimited) │ │ limit: 50,000 │
└─────────────────┘ └─────────────────┘
Per-Member Limits (Optional)¶
Team admins can set individual member limits:
$member->allow_unlimited_credits = false;
$member->remaining_words = 50000;
$member->remaining_images = 50;
Data Isolation¶
Hierarchy¶
Data privacy settings cascade through the hierarchy:
User (base consent)
└── training_data_opt_out, analytics_opt_in
│
▼ inherits (nullable = use parent)
Team
└── training_data_opt_out, analytics_opt_in
│
▼ inherits
Company
└── data_isolation_enabled, kb_cross_brand_sharing
│
▼ inherits
Product
└── training_data_opt_out (nullable = use company)
Team Boundary Enforcement¶
Resources are isolated by team:
// Good: Uses team scope
Company::forTeam()->get();
// Bad: Returns all companies (no isolation)
Company::all();
Common Patterns¶
1. Get User's Active Team Context¶
$user = auth()->user();
$team = $user->activeTeam();
if ($team) {
// User has an active team
$companies = Company::forTeam($team->id)->get();
}
2. Switch User's Active Team¶
$user->setActiveTeam($newTeam);
// Verifies user has access before switching
// Updates active_team_id in database
3. Invite User to Team¶
// Create invitation
TeamMember::create([
'team_id' => $team->id,
'email' => 'newuser@example.com',
'role' => 'member',
'status' => 'waiting',
]);
// User accepts invitation
$teamMember->update([
'user_id' => $user->id,
'status' => 'joined',
'joined_at' => now(),
]);
4. Check Resource Access¶
// Does this team have access to the company?
if ($company->teamHasAccess($teamId)) {
// Allow access
}
// Get all companies user can access
$companies = Company::accessibleByUser($user)->get();
5. Share Resource with Another Team¶
// Grant read access to another team
$company->grantTeamAccess($otherTeamId, 'read');
// Revoke access
$company->revokeTeamAccess($otherTeamId);
Migration Guide¶
For Existing Code¶
- Replace user-scoped queries with team-scoped:
// Before (user-centric)
Company::where('user_id', auth()->id())->get();
// After (team-centric)
Company::forTeam()->get();
// or
Company::accessibleByUser()->get();
- Use activeTeam() instead of team_id:
- Set team_id on resource creation:
// Automatic (via BelongsToTeam trait)
Company::create(['name' => 'Acme']); // team_id auto-set
// Explicit
Company::create([
'name' => 'Acme',
'team_id' => $user->activeTeam()->id,
]);
Database Migrations Run¶
2026_01_09_000001_add_team_id_to_companies_table.php2026_01_09_000002_add_active_team_id_to_users_table.php2026_01_09_000003_backfill_companies_team_id.php2026_01_09_000004_backfill_products_team_id.php2026_01_09_000005_backfill_users_active_team_id.php
Related Documentation¶
- Application Architecture - Overall system architecture
- Pricing Analysis - Team-level subscription model
- Knowledge Base Guide - Team-scoped documents
- Capabilities Matrix - Team credit allocation
- Team Member Capabilities - Role & capability-based permissions
Document generated: 2026-01-09