Data Model Strategy: User, Team & Company Relationships¶
Current Architecture¶
User (1) ─────┬───── (*) Company ← User owns brands directly
│
├───── (1) Team ← User creates/owns a team
│ │
│ └── (*) TeamMember ← Multiple users can join
│
└───── (?) team_id ← User's "active" team (ambiguous)
Current Issues¶
User.team_idis ambiguous - is it "owned team" or "active team"?- Company belongs to User, not Team - limits collaboration
- Partner belongs to Team, but Company doesn't - inconsistent
- One user can only "belong to" one team at a time
Recommended Future Architecture¶
Team-Centric Model (Recommended for AWS Partners)¶
Team (Organization)
│
├── TeamMember (pivot) ──── User
│ └── role: owner | admin | member | viewer
│
├── Company (Brand)
│ └── team_id (required)
│
├── Partner (Co-Sell Profile)
│ └── team_id (required)
│
└── Product (Listings)
└── team_id (required)
Key Changes¶
- Team is the tenant, not User
- User can belong to MANY teams via TeamMember (many-to-many)
- Company moves from User to Team ownership
- All resources scoped to Team
Relationship Questions & Answers¶
Can one user have many team_ids?¶
Currently: No - users.team_id is singular. But TeamMember already supports this!
Recommended: Yes, via TeamMember pivot:
// User model
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class, 'team_members')
->withPivot('role', 'status')
->wherePivot('status', 'joined');
}
// Get user's active team (could store in session/preference)
public function activeTeam(): ?Team
{
return $this->teams()->first(); // or use preference
}
What if 2 users want to join the same team?¶
Already supported! TeamMember handles this:
// Invite user to team
TeamMember::create([
'team_id' => $team->id,
'email' => 'colleague@example.com',
'role' => 'member',
'status' => 'waiting', // pending invitation
]);
// When user accepts
$teamMember->update([
'user_id' => $user->id,
'status' => 'joined',
'joined_at' => now(),
]);
Data Isolation Strategy¶
Current Hierarchy (Good Foundation)¶
User Level → training_data_opt_out, analytics_opt_in
↓ inherits
Team Level → training_data_opt_out (nullable = inherit)
↓ inherits
Company Level → data_isolation_enabled, kb_cross_brand_sharing
↓ inherits
Product Level → training_data_opt_out (nullable = inherit)
Recommended Enhancement: Explicit Scoping¶
Add explicit scoping at query level:
// Scope trait for team-based resources
trait BelongsToTeam
{
public function scopeForTeam($query, ?int $teamId = null)
{
$teamId = $teamId ?? auth()->user()->activeTeam()?->id;
return $query->where(function ($q) use ($teamId) {
$q->where('team_id', $teamId)
->orWhereJsonContains('team_access->team_ids', $teamId);
});
}
}
// Usage
Company::forTeam()->get(); // Auto-scoped to active team
Migration Plan¶
Phase 1: Add team_id to companies¶
// Migration
Schema::table('companies', function (Blueprint $table) {
$table->foreignId('team_id')->nullable()->after('user_id');
});
// Backfill: assign companies to user's primary team
Company::whereNull('team_id')->each(function ($company) {
$team = Team::where('user_id', $company->user_id)->first();
if ($team) {
$company->update(['team_id' => $team->id]);
}
});
Phase 2: Update queries to support both¶
// Transition period: support both user_id and team_id
public function scopeAccessible($query)
{
$user = auth()->user();
return $query->where(function ($q) use ($user) {
$q->where('user_id', $user->id)
->orWhere('team_id', $user->team_id)
->orWhereJsonContains('team_access->team_ids', $user->team_id);
});
}
Phase 3: Make team_id required¶
// After backfill is complete
Schema::table('companies', function (Blueprint $table) {
$table->foreignId('team_id')->nullable(false)->change();
});
Summary: Recommended Changes¶
| Priority | Change | Effort | Impact |
|---|---|---|---|
| High | Add team_id to companies table |
Low | Enables team collaboration on brands |
| High | Fix User.team() relationship logic |
Low | Fixes current bugs |
| Medium | Add active_team_id to users (or session) |
Low | Multi-team UX |
| Medium | Create BelongsToTeam trait for scoping |
Medium | Consistent data isolation |
| Low | Remove singular users.team_id |
Medium | Cleanup after migration |
Current Table Relationships¶
| Relationship | Type | Parent | Child | Notes |
|---|---|---|---|---|
| User → Team (created) | One-to-Many | User.id | Team.user_id | User can create multiple teams |
| User → TeamMember | One-to-Many | User.id | TeamMember.user_id | Nullable, allows pending invites |
| Team → TeamMembers | One-to-Many | Team.id | TeamMember.team_id | CASCADE delete |
| User → Company | One-to-Many | User.id | Company.user_id | User owns companies |
| Company → Products | One-to-Many | Company.id | Product.company_id | Products belong to companies |
| Product → Team | Many-to-One | Team.id | Product.team_id | Products can be team-assigned |
| Team → Partners | One-to-Many | Team.id | Partner.team_id | Co-sell partners per team |
| Partner ↔ Partner | Many-to-Many | - | cosell_relationships | Co-sell relationships |
Key Files¶
app/Models/User.php- User model with team relationshipsapp/Models/Team/Team.php- Team modelapp/Models/Team/TeamMember.php- TeamMember pivot modelapp/Models/Company.php- Company model with data isolationapp/Models/Product.php- Product model with team accessapp/Extensions/ContentManager/System/Models/Partner.php- Partner model
Last updated: January 2026