tenseleyflow/shithub / 6aaaf84

Browse files

S30: orgs schema + members + invitations + principals + sqlc

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6aaaf843ed80b05bcb67826b05510f2421de2b37
Parents
6e99c48
Tree
64e64fd

21 changed files

StatusFile+-
M internal/auth/policy/sqlc/models.go 259 0
M internal/checks/sqlc/models.go 259 0
M internal/issues/sqlc/models.go 259 0
M internal/meta/sqlc/models.go 259 0
A internal/migrationsfs/migrations/0034_orgs.sql 215 0
M internal/notif/sqlc/models.go 175 0
A internal/orgs/queries/invitations.sql 80 0
A internal/orgs/queries/members.sql 45 0
A internal/orgs/queries/orgs.sql 64 0
A internal/orgs/sqlc/db.go 25 0
A internal/orgs/sqlc/invitations.sql.go 378 0
A internal/orgs/sqlc/members.sql.go 200 0
C internal/orgs/sqlc/models.go 0 0
A internal/orgs/sqlc/orgs.sql.go 267 0
A internal/orgs/sqlc/querier.go 75 0
M internal/pulls/sqlc/models.go 259 0
M internal/repos/sqlc/models.go 259 0
M internal/social/sqlc/models.go 259 0
M internal/users/sqlc/models.go 259 0
M internal/worker/sqlc/models.go 259 0
M sqlc.yaml 32 0
internal/auth/policy/sqlc/models.gomodified
@@ -362,6 +362,133 @@ func (ns NullMilestoneState) Value() (driver.Value, error) {
362362
 	return string(ns.MilestoneState), nil
363363
 }
364364
 
365
+type NotificationThreadKind string
366
+
367
+const (
368
+	NotificationThreadKindIssue NotificationThreadKind = "issue"
369
+	NotificationThreadKindPr    NotificationThreadKind = "pr"
370
+)
371
+
372
+func (e *NotificationThreadKind) Scan(src interface{}) error {
373
+	switch s := src.(type) {
374
+	case []byte:
375
+		*e = NotificationThreadKind(s)
376
+	case string:
377
+		*e = NotificationThreadKind(s)
378
+	default:
379
+		return fmt.Errorf("unsupported scan type for NotificationThreadKind: %T", src)
380
+	}
381
+	return nil
382
+}
383
+
384
+type NullNotificationThreadKind struct {
385
+	NotificationThreadKind NotificationThreadKind
386
+	Valid                  bool // Valid is true if NotificationThreadKind is not NULL
387
+}
388
+
389
+// Scan implements the Scanner interface.
390
+func (ns *NullNotificationThreadKind) Scan(value interface{}) error {
391
+	if value == nil {
392
+		ns.NotificationThreadKind, ns.Valid = "", false
393
+		return nil
394
+	}
395
+	ns.Valid = true
396
+	return ns.NotificationThreadKind.Scan(value)
397
+}
398
+
399
+// Value implements the driver Valuer interface.
400
+func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
401
+	if !ns.Valid {
402
+		return nil, nil
403
+	}
404
+	return string(ns.NotificationThreadKind), nil
405
+}
406
+
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
365492
 type PrFileStatus string
366493
 
367494
 const (
@@ -580,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
580707
 	return string(ns.PrReviewState), nil
581708
 }
582709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
583752
 type RepoInitStatus string
584753
 
585754
 const (
@@ -887,6 +1056,12 @@ type DomainEvent struct {
8871056
 	CreatedAt   pgtype.Timestamptz
8881057
 }
8891058
 
1059
+type DomainEventsProcessed struct {
1060
+	Consumer    string
1061
+	LastEventID int64
1062
+	UpdatedAt   pgtype.Timestamptz
1063
+}
1064
+
8901065
 type EmailVerification struct {
8911066
 	ID          int64
8921067
 	UserEmailID int64
@@ -1013,6 +1188,84 @@ type Milestone struct {
10131188
 	ClosedAt    pgtype.Timestamptz
10141189
 }
10151190
 
1191
+type Notification struct {
1192
+	ID              int64
1193
+	RecipientUserID int64
1194
+	Kind            string
1195
+	Reason          string
1196
+	RepoID          pgtype.Int8
1197
+	ThreadKind      NullNotificationThreadKind
1198
+	ThreadID        pgtype.Int8
1199
+	SourceEventID   pgtype.Int8
1200
+	Unread          bool
1201
+	LastEventAt     pgtype.Timestamptz
1202
+	LastActorUserID pgtype.Int8
1203
+	Summary         []byte
1204
+	CreatedAt       pgtype.Timestamptz
1205
+	UpdatedAt       pgtype.Timestamptz
1206
+}
1207
+
1208
+type NotificationEmailLog struct {
1209
+	ID              int64
1210
+	RecipientUserID int64
1211
+	NotificationID  pgtype.Int8
1212
+	ThreadKind      NullNotificationThreadKind
1213
+	ThreadID        pgtype.Int8
1214
+	SentAt          pgtype.Timestamptz
1215
+	MessageID       pgtype.Text
1216
+}
1217
+
1218
+type NotificationThread struct {
1219
+	RecipientUserID int64
1220
+	ThreadKind      NotificationThreadKind
1221
+	ThreadID        int64
1222
+	Subscribed      bool
1223
+	Reason          string
1224
+	UpdatedAt       pgtype.Timestamptz
1225
+}
1226
+
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
10161269
 type PasswordReset struct {
10171270
 	ID        int64
10181271
 	UserID    int64
@@ -1068,6 +1321,12 @@ type PrReviewRequest struct {
10681321
 	SatisfiedByReviewID pgtype.Int8
10691322
 }
10701323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
10711330
 type PullRequest struct {
10721331
 	IssueID            int64
10731332
 	BaseRef            string
internal/checks/sqlc/models.gomodified
@@ -362,6 +362,133 @@ func (ns NullMilestoneState) Value() (driver.Value, error) {
362362
 	return string(ns.MilestoneState), nil
363363
 }
364364
 
365
+type NotificationThreadKind string
366
+
367
+const (
368
+	NotificationThreadKindIssue NotificationThreadKind = "issue"
369
+	NotificationThreadKindPr    NotificationThreadKind = "pr"
370
+)
371
+
372
+func (e *NotificationThreadKind) Scan(src interface{}) error {
373
+	switch s := src.(type) {
374
+	case []byte:
375
+		*e = NotificationThreadKind(s)
376
+	case string:
377
+		*e = NotificationThreadKind(s)
378
+	default:
379
+		return fmt.Errorf("unsupported scan type for NotificationThreadKind: %T", src)
380
+	}
381
+	return nil
382
+}
383
+
384
+type NullNotificationThreadKind struct {
385
+	NotificationThreadKind NotificationThreadKind
386
+	Valid                  bool // Valid is true if NotificationThreadKind is not NULL
387
+}
388
+
389
+// Scan implements the Scanner interface.
390
+func (ns *NullNotificationThreadKind) Scan(value interface{}) error {
391
+	if value == nil {
392
+		ns.NotificationThreadKind, ns.Valid = "", false
393
+		return nil
394
+	}
395
+	ns.Valid = true
396
+	return ns.NotificationThreadKind.Scan(value)
397
+}
398
+
399
+// Value implements the driver Valuer interface.
400
+func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
401
+	if !ns.Valid {
402
+		return nil, nil
403
+	}
404
+	return string(ns.NotificationThreadKind), nil
405
+}
406
+
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
365492
 type PrFileStatus string
366493
 
367494
 const (
@@ -580,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
580707
 	return string(ns.PrReviewState), nil
581708
 }
582709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
583752
 type RepoInitStatus string
584753
 
585754
 const (
@@ -887,6 +1056,12 @@ type DomainEvent struct {
8871056
 	CreatedAt   pgtype.Timestamptz
8881057
 }
8891058
 
1059
+type DomainEventsProcessed struct {
1060
+	Consumer    string
1061
+	LastEventID int64
1062
+	UpdatedAt   pgtype.Timestamptz
1063
+}
1064
+
8901065
 type EmailVerification struct {
8911066
 	ID          int64
8921067
 	UserEmailID int64
@@ -1013,6 +1188,84 @@ type Milestone struct {
10131188
 	ClosedAt    pgtype.Timestamptz
10141189
 }
10151190
 
1191
+type Notification struct {
1192
+	ID              int64
1193
+	RecipientUserID int64
1194
+	Kind            string
1195
+	Reason          string
1196
+	RepoID          pgtype.Int8
1197
+	ThreadKind      NullNotificationThreadKind
1198
+	ThreadID        pgtype.Int8
1199
+	SourceEventID   pgtype.Int8
1200
+	Unread          bool
1201
+	LastEventAt     pgtype.Timestamptz
1202
+	LastActorUserID pgtype.Int8
1203
+	Summary         []byte
1204
+	CreatedAt       pgtype.Timestamptz
1205
+	UpdatedAt       pgtype.Timestamptz
1206
+}
1207
+
1208
+type NotificationEmailLog struct {
1209
+	ID              int64
1210
+	RecipientUserID int64
1211
+	NotificationID  pgtype.Int8
1212
+	ThreadKind      NullNotificationThreadKind
1213
+	ThreadID        pgtype.Int8
1214
+	SentAt          pgtype.Timestamptz
1215
+	MessageID       pgtype.Text
1216
+}
1217
+
1218
+type NotificationThread struct {
1219
+	RecipientUserID int64
1220
+	ThreadKind      NotificationThreadKind
1221
+	ThreadID        int64
1222
+	Subscribed      bool
1223
+	Reason          string
1224
+	UpdatedAt       pgtype.Timestamptz
1225
+}
1226
+
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
10161269
 type PasswordReset struct {
10171270
 	ID        int64
10181271
 	UserID    int64
@@ -1068,6 +1321,12 @@ type PrReviewRequest struct {
10681321
 	SatisfiedByReviewID pgtype.Int8
10691322
 }
10701323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
10711330
 type PullRequest struct {
10721331
 	IssueID            int64
10731332
 	BaseRef            string
internal/issues/sqlc/models.gomodified
@@ -362,6 +362,133 @@ func (ns NullMilestoneState) Value() (driver.Value, error) {
362362
 	return string(ns.MilestoneState), nil
363363
 }
364364
 
365
+type NotificationThreadKind string
366
+
367
+const (
368
+	NotificationThreadKindIssue NotificationThreadKind = "issue"
369
+	NotificationThreadKindPr    NotificationThreadKind = "pr"
370
+)
371
+
372
+func (e *NotificationThreadKind) Scan(src interface{}) error {
373
+	switch s := src.(type) {
374
+	case []byte:
375
+		*e = NotificationThreadKind(s)
376
+	case string:
377
+		*e = NotificationThreadKind(s)
378
+	default:
379
+		return fmt.Errorf("unsupported scan type for NotificationThreadKind: %T", src)
380
+	}
381
+	return nil
382
+}
383
+
384
+type NullNotificationThreadKind struct {
385
+	NotificationThreadKind NotificationThreadKind
386
+	Valid                  bool // Valid is true if NotificationThreadKind is not NULL
387
+}
388
+
389
+// Scan implements the Scanner interface.
390
+func (ns *NullNotificationThreadKind) Scan(value interface{}) error {
391
+	if value == nil {
392
+		ns.NotificationThreadKind, ns.Valid = "", false
393
+		return nil
394
+	}
395
+	ns.Valid = true
396
+	return ns.NotificationThreadKind.Scan(value)
397
+}
398
+
399
+// Value implements the driver Valuer interface.
400
+func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
401
+	if !ns.Valid {
402
+		return nil, nil
403
+	}
404
+	return string(ns.NotificationThreadKind), nil
405
+}
406
+
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
365492
 type PrFileStatus string
366493
 
367494
 const (
@@ -580,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
580707
 	return string(ns.PrReviewState), nil
581708
 }
582709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
583752
 type RepoInitStatus string
584753
 
585754
 const (
@@ -887,6 +1056,12 @@ type DomainEvent struct {
8871056
 	CreatedAt   pgtype.Timestamptz
8881057
 }
8891058
 
1059
+type DomainEventsProcessed struct {
1060
+	Consumer    string
1061
+	LastEventID int64
1062
+	UpdatedAt   pgtype.Timestamptz
1063
+}
1064
+
8901065
 type EmailVerification struct {
8911066
 	ID          int64
8921067
 	UserEmailID int64
@@ -1013,6 +1188,84 @@ type Milestone struct {
10131188
 	ClosedAt    pgtype.Timestamptz
10141189
 }
10151190
 
1191
+type Notification struct {
1192
+	ID              int64
1193
+	RecipientUserID int64
1194
+	Kind            string
1195
+	Reason          string
1196
+	RepoID          pgtype.Int8
1197
+	ThreadKind      NullNotificationThreadKind
1198
+	ThreadID        pgtype.Int8
1199
+	SourceEventID   pgtype.Int8
1200
+	Unread          bool
1201
+	LastEventAt     pgtype.Timestamptz
1202
+	LastActorUserID pgtype.Int8
1203
+	Summary         []byte
1204
+	CreatedAt       pgtype.Timestamptz
1205
+	UpdatedAt       pgtype.Timestamptz
1206
+}
1207
+
1208
+type NotificationEmailLog struct {
1209
+	ID              int64
1210
+	RecipientUserID int64
1211
+	NotificationID  pgtype.Int8
1212
+	ThreadKind      NullNotificationThreadKind
1213
+	ThreadID        pgtype.Int8
1214
+	SentAt          pgtype.Timestamptz
1215
+	MessageID       pgtype.Text
1216
+}
1217
+
1218
+type NotificationThread struct {
1219
+	RecipientUserID int64
1220
+	ThreadKind      NotificationThreadKind
1221
+	ThreadID        int64
1222
+	Subscribed      bool
1223
+	Reason          string
1224
+	UpdatedAt       pgtype.Timestamptz
1225
+}
1226
+
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
10161269
 type PasswordReset struct {
10171270
 	ID        int64
10181271
 	UserID    int64
@@ -1068,6 +1321,12 @@ type PrReviewRequest struct {
10681321
 	SatisfiedByReviewID pgtype.Int8
10691322
 }
10701323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
10711330
 type PullRequest struct {
10721331
 	IssueID            int64
10731332
 	BaseRef            string
internal/meta/sqlc/models.gomodified
@@ -362,6 +362,133 @@ func (ns NullMilestoneState) Value() (driver.Value, error) {
362362
 	return string(ns.MilestoneState), nil
363363
 }
364364
 
365
+type NotificationThreadKind string
366
+
367
+const (
368
+	NotificationThreadKindIssue NotificationThreadKind = "issue"
369
+	NotificationThreadKindPr    NotificationThreadKind = "pr"
370
+)
371
+
372
+func (e *NotificationThreadKind) Scan(src interface{}) error {
373
+	switch s := src.(type) {
374
+	case []byte:
375
+		*e = NotificationThreadKind(s)
376
+	case string:
377
+		*e = NotificationThreadKind(s)
378
+	default:
379
+		return fmt.Errorf("unsupported scan type for NotificationThreadKind: %T", src)
380
+	}
381
+	return nil
382
+}
383
+
384
+type NullNotificationThreadKind struct {
385
+	NotificationThreadKind NotificationThreadKind
386
+	Valid                  bool // Valid is true if NotificationThreadKind is not NULL
387
+}
388
+
389
+// Scan implements the Scanner interface.
390
+func (ns *NullNotificationThreadKind) Scan(value interface{}) error {
391
+	if value == nil {
392
+		ns.NotificationThreadKind, ns.Valid = "", false
393
+		return nil
394
+	}
395
+	ns.Valid = true
396
+	return ns.NotificationThreadKind.Scan(value)
397
+}
398
+
399
+// Value implements the driver Valuer interface.
400
+func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
401
+	if !ns.Valid {
402
+		return nil, nil
403
+	}
404
+	return string(ns.NotificationThreadKind), nil
405
+}
406
+
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
365492
 type PrFileStatus string
366493
 
367494
 const (
@@ -580,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
580707
 	return string(ns.PrReviewState), nil
581708
 }
582709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
583752
 type RepoInitStatus string
584753
 
585754
 const (
@@ -887,6 +1056,12 @@ type DomainEvent struct {
8871056
 	CreatedAt   pgtype.Timestamptz
8881057
 }
8891058
 
1059
+type DomainEventsProcessed struct {
1060
+	Consumer    string
1061
+	LastEventID int64
1062
+	UpdatedAt   pgtype.Timestamptz
1063
+}
1064
+
8901065
 type EmailVerification struct {
8911066
 	ID          int64
8921067
 	UserEmailID int64
@@ -1013,6 +1188,84 @@ type Milestone struct {
10131188
 	ClosedAt    pgtype.Timestamptz
10141189
 }
10151190
 
1191
+type Notification struct {
1192
+	ID              int64
1193
+	RecipientUserID int64
1194
+	Kind            string
1195
+	Reason          string
1196
+	RepoID          pgtype.Int8
1197
+	ThreadKind      NullNotificationThreadKind
1198
+	ThreadID        pgtype.Int8
1199
+	SourceEventID   pgtype.Int8
1200
+	Unread          bool
1201
+	LastEventAt     pgtype.Timestamptz
1202
+	LastActorUserID pgtype.Int8
1203
+	Summary         []byte
1204
+	CreatedAt       pgtype.Timestamptz
1205
+	UpdatedAt       pgtype.Timestamptz
1206
+}
1207
+
1208
+type NotificationEmailLog struct {
1209
+	ID              int64
1210
+	RecipientUserID int64
1211
+	NotificationID  pgtype.Int8
1212
+	ThreadKind      NullNotificationThreadKind
1213
+	ThreadID        pgtype.Int8
1214
+	SentAt          pgtype.Timestamptz
1215
+	MessageID       pgtype.Text
1216
+}
1217
+
1218
+type NotificationThread struct {
1219
+	RecipientUserID int64
1220
+	ThreadKind      NotificationThreadKind
1221
+	ThreadID        int64
1222
+	Subscribed      bool
1223
+	Reason          string
1224
+	UpdatedAt       pgtype.Timestamptz
1225
+}
1226
+
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
10161269
 type PasswordReset struct {
10171270
 	ID        int64
10181271
 	UserID    int64
@@ -1068,6 +1321,12 @@ type PrReviewRequest struct {
10681321
 	SatisfiedByReviewID pgtype.Int8
10691322
 }
10701323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
10711330
 type PullRequest struct {
10721331
 	IssueID            int64
10731332
 	BaseRef            string
internal/migrationsfs/migrations/0034_orgs.sqladded
@@ -0,0 +1,215 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+--
3
+-- S30 — Organizations.
4
+--
5
+-- Schema overview:
6
+--
7
+--   orgs              — first-class principals that can own repos.
8
+--   org_members       — (org, user) ↔ role; PK on the pair so a user
9
+--                       holds exactly one role per org.
10
+--   org_invitations   — pending invites by username OR email; one of
11
+--                       target_user_id / target_email is set.
12
+--   principals        — single source of truth for /{slug} resolution.
13
+--                       Maintained by triggers on users + orgs so a
14
+--                       slug collision is structurally impossible.
15
+--
16
+-- The `repos.owner_org_id` column already exists from 0017 with the
17
+-- right XOR CHECK constraint; this migration just adds the FK once
18
+-- the orgs table exists.
19
+
20
+-- +goose Up
21
+
22
+-- ─── orgs ───────────────────────────────────────────────────────────
23
+CREATE TYPE org_plan AS ENUM ('free', 'team', 'enterprise');
24
+
25
+CREATE TABLE orgs (
26
+    id                       bigserial   PRIMARY KEY,
27
+    slug                     citext      NOT NULL UNIQUE,
28
+    display_name             text        NOT NULL DEFAULT '',
29
+    description              text        NOT NULL DEFAULT '',
30
+    avatar_object_key        text,
31
+    location                 text        NOT NULL DEFAULT '',
32
+    website                  text        NOT NULL DEFAULT '',
33
+    billing_email            text        NOT NULL DEFAULT '',
34
+    plan                     org_plan    NOT NULL DEFAULT 'free',
35
+    allow_member_repo_create boolean     NOT NULL DEFAULT true,
36
+    created_by_user_id       bigint      REFERENCES users(id) ON DELETE SET NULL,
37
+    suspended_at             timestamptz,
38
+    suspended_reason         text,
39
+    deleted_at               timestamptz,
40
+    created_at               timestamptz NOT NULL DEFAULT now(),
41
+    updated_at               timestamptz NOT NULL DEFAULT now(),
42
+
43
+    CONSTRAINT orgs_slug_length CHECK (char_length(slug::text) BETWEEN 1 AND 39),
44
+    CONSTRAINT orgs_description_length CHECK (char_length(description) <= 350)
45
+);
46
+
47
+CREATE INDEX orgs_deleted_at_idx ON orgs (deleted_at) WHERE deleted_at IS NOT NULL;
48
+CREATE INDEX orgs_suspended_at_idx ON orgs (suspended_at) WHERE suspended_at IS NOT NULL;
49
+
50
+CREATE TRIGGER set_updated_at BEFORE UPDATE ON orgs
51
+    FOR EACH ROW EXECUTE FUNCTION tg_set_updated_at();
52
+
53
+-- ─── org_members ───────────────────────────────────────────────────
54
+CREATE TYPE org_role AS ENUM ('owner', 'member');
55
+
56
+CREATE TABLE org_members (
57
+    org_id              bigint      NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
58
+    user_id             bigint      NOT NULL REFERENCES users(id) ON DELETE CASCADE,
59
+    role                org_role    NOT NULL DEFAULT 'member',
60
+    invited_by_user_id  bigint      REFERENCES users(id) ON DELETE SET NULL,
61
+    joined_at           timestamptz NOT NULL DEFAULT now(),
62
+
63
+    PRIMARY KEY (org_id, user_id)
64
+);
65
+
66
+CREATE INDEX org_members_user_idx ON org_members (user_id);
67
+CREATE INDEX org_members_role_idx ON org_members (org_id, role);
68
+
69
+-- ─── org_invitations ───────────────────────────────────────────────
70
+CREATE TABLE org_invitations (
71
+    id                   bigserial   PRIMARY KEY,
72
+    org_id               bigint      NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
73
+    invited_by_user_id   bigint      REFERENCES users(id) ON DELETE SET NULL,
74
+    target_user_id       bigint      REFERENCES users(id) ON DELETE CASCADE,
75
+    target_email         citext,
76
+    role                 org_role    NOT NULL DEFAULT 'member',
77
+    token_hash           bytea       NOT NULL UNIQUE,
78
+    expires_at           timestamptz NOT NULL,
79
+    accepted_at          timestamptz,
80
+    declined_at          timestamptz,
81
+    canceled_at          timestamptz,
82
+    created_at           timestamptz NOT NULL DEFAULT now(),
83
+
84
+    CONSTRAINT org_invites_target_xor CHECK (
85
+        (target_user_id IS NOT NULL AND target_email IS NULL)
86
+     OR (target_user_id IS NULL     AND target_email IS NOT NULL)
87
+    )
88
+);
89
+
90
+CREATE INDEX org_invites_org_pending_idx
91
+    ON org_invitations (org_id)
92
+    WHERE accepted_at IS NULL AND declined_at IS NULL AND canceled_at IS NULL;
93
+
94
+CREATE INDEX org_invites_target_user_idx
95
+    ON org_invitations (target_user_id)
96
+    WHERE target_user_id IS NOT NULL;
97
+
98
+CREATE INDEX org_invites_target_email_idx
99
+    ON org_invitations (target_email)
100
+    WHERE target_email IS NOT NULL;
101
+
102
+-- ─── principals ────────────────────────────────────────────────────
103
+-- Unifies the /{slug} URL space across users and orgs. Maintained by
104
+-- AFTER triggers on users + orgs so the row is always coherent.
105
+-- One row per (slug, kind, id); slug PK enforces global uniqueness
106
+-- across both tables — a slug collision is structurally impossible.
107
+CREATE TYPE principal_kind AS ENUM ('user', 'org');
108
+
109
+CREATE TABLE principals (
110
+    slug citext         PRIMARY KEY,
111
+    kind principal_kind NOT NULL,
112
+    id   bigint         NOT NULL,
113
+
114
+    -- Sanity index for "all orgs" / "all users" sweeps.
115
+    CONSTRAINT principals_id_kind_idx UNIQUE (kind, id)
116
+);
117
+
118
+-- Backfill from existing users.
119
+INSERT INTO principals (slug, kind, id)
120
+SELECT username, 'user'::principal_kind, id
121
+FROM users
122
+WHERE deleted_at IS NULL
123
+ON CONFLICT (slug) DO NOTHING;
124
+
125
+-- +goose StatementBegin
126
+CREATE OR REPLACE FUNCTION tg_principals_user_sync() RETURNS trigger AS $$
127
+BEGIN
128
+    IF TG_OP = 'DELETE' THEN
129
+        DELETE FROM principals WHERE kind = 'user' AND id = OLD.id;
130
+        RETURN OLD;
131
+    END IF;
132
+    IF TG_OP = 'INSERT' THEN
133
+        INSERT INTO principals (slug, kind, id)
134
+        VALUES (NEW.username, 'user', NEW.id);
135
+        RETURN NEW;
136
+    END IF;
137
+    -- UPDATE: handle username change + soft-delete flip.
138
+    IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN
139
+        DELETE FROM principals WHERE kind = 'user' AND id = NEW.id;
140
+        RETURN NEW;
141
+    END IF;
142
+    IF NEW.deleted_at IS NULL AND OLD.deleted_at IS NOT NULL THEN
143
+        INSERT INTO principals (slug, kind, id)
144
+        VALUES (NEW.username, 'user', NEW.id)
145
+        ON CONFLICT (slug) DO UPDATE SET kind = EXCLUDED.kind, id = EXCLUDED.id;
146
+        RETURN NEW;
147
+    END IF;
148
+    IF NEW.username <> OLD.username THEN
149
+        UPDATE principals SET slug = NEW.username
150
+            WHERE kind = 'user' AND id = NEW.id;
151
+    END IF;
152
+    RETURN NEW;
153
+END;
154
+$$ LANGUAGE plpgsql;
155
+-- +goose StatementEnd
156
+
157
+-- +goose StatementBegin
158
+CREATE OR REPLACE FUNCTION tg_principals_org_sync() RETURNS trigger AS $$
159
+BEGIN
160
+    IF TG_OP = 'DELETE' THEN
161
+        DELETE FROM principals WHERE kind = 'org' AND id = OLD.id;
162
+        RETURN OLD;
163
+    END IF;
164
+    IF TG_OP = 'INSERT' THEN
165
+        INSERT INTO principals (slug, kind, id)
166
+        VALUES (NEW.slug, 'org', NEW.id);
167
+        RETURN NEW;
168
+    END IF;
169
+    IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN
170
+        DELETE FROM principals WHERE kind = 'org' AND id = NEW.id;
171
+        RETURN NEW;
172
+    END IF;
173
+    IF NEW.deleted_at IS NULL AND OLD.deleted_at IS NOT NULL THEN
174
+        INSERT INTO principals (slug, kind, id)
175
+        VALUES (NEW.slug, 'org', NEW.id)
176
+        ON CONFLICT (slug) DO UPDATE SET kind = EXCLUDED.kind, id = EXCLUDED.id;
177
+        RETURN NEW;
178
+    END IF;
179
+    IF NEW.slug <> OLD.slug THEN
180
+        UPDATE principals SET slug = NEW.slug
181
+            WHERE kind = 'org' AND id = NEW.id;
182
+    END IF;
183
+    RETURN NEW;
184
+END;
185
+$$ LANGUAGE plpgsql;
186
+-- +goose StatementEnd
187
+
188
+CREATE TRIGGER tg_principals_user_sync_iud
189
+    AFTER INSERT OR UPDATE OR DELETE ON users
190
+    FOR EACH ROW EXECUTE FUNCTION tg_principals_user_sync();
191
+
192
+CREATE TRIGGER tg_principals_org_sync_iud
193
+    AFTER INSERT OR UPDATE OR DELETE ON orgs
194
+    FOR EACH ROW EXECUTE FUNCTION tg_principals_org_sync();
195
+
196
+-- ─── repos.owner_org_id FK ─────────────────────────────────────────
197
+-- The column existed from 0017 with the XOR CHECK already in place;
198
+-- adding the actual FK only now that the target table exists.
199
+ALTER TABLE repos
200
+    ADD CONSTRAINT repos_owner_org_id_fkey
201
+        FOREIGN KEY (owner_org_id) REFERENCES orgs(id) ON DELETE CASCADE;
202
+
203
+-- +goose Down
204
+ALTER TABLE repos DROP CONSTRAINT IF EXISTS repos_owner_org_id_fkey;
205
+DROP TRIGGER IF EXISTS tg_principals_org_sync_iud ON orgs;
206
+DROP TRIGGER IF EXISTS tg_principals_user_sync_iud ON users;
207
+DROP FUNCTION IF EXISTS tg_principals_org_sync();
208
+DROP FUNCTION IF EXISTS tg_principals_user_sync();
209
+DROP TABLE IF EXISTS principals;
210
+DROP TYPE IF EXISTS principal_kind;
211
+DROP TABLE IF EXISTS org_invitations;
212
+DROP TABLE IF EXISTS org_members;
213
+DROP TYPE IF EXISTS org_role;
214
+DROP TABLE IF EXISTS orgs;
215
+DROP TYPE IF EXISTS org_plan;
internal/notif/sqlc/models.gomodified
@@ -404,6 +404,91 @@ func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
404404
 	return string(ns.NotificationThreadKind), nil
405405
 }
406406
 
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
407492
 type PrFileStatus string
408493
 
409494
 const (
@@ -622,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
622707
 	return string(ns.PrReviewState), nil
623708
 }
624709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
625752
 type RepoInitStatus string
626753
 
627754
 const (
@@ -1097,6 +1224,48 @@ type NotificationThread struct {
10971224
 	UpdatedAt       pgtype.Timestamptz
10981225
 }
10991226
 
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
11001269
 type PasswordReset struct {
11011270
 	ID        int64
11021271
 	UserID    int64
@@ -1152,6 +1321,12 @@ type PrReviewRequest struct {
11521321
 	SatisfiedByReviewID pgtype.Int8
11531322
 }
11541323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
11551330
 type PullRequest struct {
11561331
 	IssueID            int64
11571332
 	BaseRef            string
internal/orgs/queries/invitations.sqladded
@@ -0,0 +1,80 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+-- ─── org_invitations ───────────────────────────────────────────────
4
+
5
+-- name: CreateOrgInvitation :one
6
+INSERT INTO org_invitations (
7
+    org_id, invited_by_user_id, target_user_id, target_email,
8
+    role, token_hash, expires_at
9
+) VALUES (
10
+    $1, $2, sqlc.narg(target_user_id)::bigint, sqlc.narg(target_email)::citext,
11
+    $3, $4, $5
12
+)
13
+RETURNING *;
14
+
15
+-- name: GetOrgInvitationByTokenHash :one
16
+SELECT * FROM org_invitations WHERE token_hash = $1;
17
+
18
+-- name: GetOrgInvitationByID :one
19
+SELECT * FROM org_invitations WHERE id = $1;
20
+
21
+-- name: ListPendingInvitationsForOrg :many
22
+SELECT i.*, u.username AS target_username, ib.username AS invited_by_username
23
+FROM org_invitations i
24
+LEFT JOIN users u  ON u.id  = i.target_user_id
25
+LEFT JOIN users ib ON ib.id = i.invited_by_user_id
26
+WHERE i.org_id = $1
27
+  AND i.accepted_at IS NULL
28
+  AND i.declined_at IS NULL
29
+  AND i.canceled_at IS NULL
30
+  AND i.expires_at > now()
31
+ORDER BY i.created_at DESC;
32
+
33
+-- name: ListPendingInvitationsForUser :many
34
+-- Two flavors: by user_id (already-claimed invites) and by email
35
+-- (claim-on-signup). Caller unions the two lookups when surfacing
36
+-- to the user.
37
+SELECT i.*, o.slug AS org_slug, o.display_name AS org_display_name
38
+FROM org_invitations i
39
+JOIN orgs o ON o.id = i.org_id
40
+WHERE i.target_user_id = $1
41
+  AND i.accepted_at IS NULL
42
+  AND i.declined_at IS NULL
43
+  AND i.canceled_at IS NULL
44
+  AND i.expires_at > now()
45
+  AND o.deleted_at IS NULL
46
+ORDER BY i.created_at DESC;
47
+
48
+-- name: ListPendingInvitationsForEmail :many
49
+SELECT i.*, o.slug AS org_slug, o.display_name AS org_display_name
50
+FROM org_invitations i
51
+JOIN orgs o ON o.id = i.org_id
52
+WHERE i.target_email = $1
53
+  AND i.accepted_at IS NULL
54
+  AND i.declined_at IS NULL
55
+  AND i.canceled_at IS NULL
56
+  AND i.expires_at > now()
57
+  AND o.deleted_at IS NULL
58
+ORDER BY i.created_at DESC;
59
+
60
+-- name: AcceptOrgInvitation :exec
61
+UPDATE org_invitations SET accepted_at = now() WHERE id = $1;
62
+
63
+-- name: DeclineOrgInvitation :exec
64
+UPDATE org_invitations SET declined_at = now() WHERE id = $1;
65
+
66
+-- name: CancelOrgInvitation :exec
67
+UPDATE org_invitations SET canceled_at = now() WHERE id = $1;
68
+
69
+-- name: GetExistingPendingInvitation :one
70
+-- Idempotency check before creating a new invite — so a re-issued
71
+-- invite to the same target doesn't accumulate stale rows.
72
+SELECT * FROM org_invitations
73
+WHERE org_id = $1
74
+  AND ( (target_user_id = sqlc.narg(target_user_id)::bigint)
75
+     OR (target_email   = sqlc.narg(target_email)::citext) )
76
+  AND accepted_at IS NULL
77
+  AND declined_at IS NULL
78
+  AND canceled_at IS NULL
79
+  AND expires_at > now()
80
+LIMIT 1;
internal/orgs/queries/members.sqladded
@@ -0,0 +1,45 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+-- ─── org_members ───────────────────────────────────────────────────
4
+
5
+-- name: AddOrgMember :exec
6
+-- Idempotent on (org_id, user_id): re-adding a member is a no-op
7
+-- rather than an error. The role is taken from the supplied row when
8
+-- the member is new; existing rows keep their current role (use
9
+-- ChangeOrgMemberRole to update).
10
+INSERT INTO org_members (org_id, user_id, role, invited_by_user_id)
11
+VALUES ($1, $2, $3, sqlc.narg(invited_by_user_id)::bigint)
12
+ON CONFLICT (org_id, user_id) DO NOTHING;
13
+
14
+-- name: RemoveOrgMember :exec
15
+DELETE FROM org_members WHERE org_id = $1 AND user_id = $2;
16
+
17
+-- name: ChangeOrgMemberRole :exec
18
+UPDATE org_members SET role = $3 WHERE org_id = $1 AND user_id = $2;
19
+
20
+-- name: GetOrgMember :one
21
+SELECT * FROM org_members WHERE org_id = $1 AND user_id = $2;
22
+
23
+-- name: ListOrgMembers :many
24
+-- Members of an org with usernames + roles for the people page.
25
+SELECT m.org_id, m.user_id, m.role, m.invited_by_user_id, m.joined_at,
26
+       u.username, u.display_name
27
+FROM org_members m
28
+JOIN users u ON u.id = m.user_id
29
+WHERE m.org_id = $1
30
+  AND u.deleted_at IS NULL
31
+ORDER BY m.role ASC, u.username ASC;
32
+
33
+-- name: CountOrgOwners :one
34
+-- Used by the last-owner protection: refuses to remove or demote the
35
+-- only owner. Caller compares `count = 1` before allowing the change.
36
+SELECT count(*) FROM org_members WHERE org_id = $1 AND role = 'owner';
37
+
38
+-- name: ListOrgsForUser :many
39
+-- Profile-page input: every org a user is a member of, with role.
40
+SELECT m.org_id, m.role, o.slug, o.display_name, o.avatar_object_key
41
+FROM org_members m
42
+JOIN orgs o ON o.id = m.org_id
43
+WHERE m.user_id = $1
44
+  AND o.deleted_at IS NULL
45
+ORDER BY o.slug ASC;
internal/orgs/queries/orgs.sqladded
@@ -0,0 +1,64 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+-- ─── orgs ──────────────────────────────────────────────────────────
4
+
5
+-- name: CreateOrg :one
6
+-- Inserts a new org. The trigger on `orgs` populates `principals` for
7
+-- /{slug} resolution; the caller adds the creator as owner in the same
8
+-- tx (separate query so the orchestrator owns ordering).
9
+INSERT INTO orgs (slug, display_name, description, billing_email, created_by_user_id)
10
+VALUES ($1, $2, $3, $4, sqlc.narg(created_by_user_id)::bigint)
11
+RETURNING *;
12
+
13
+-- name: GetOrgByID :one
14
+SELECT * FROM orgs WHERE id = $1;
15
+
16
+-- name: GetOrgBySlug :one
17
+-- Slug is citext so the comparison is case-insensitive. Soft-deleted
18
+-- orgs are filtered out so a slug freed by deletion is invisible to
19
+-- normal lookups (the resolver still sees the row via the deleted_at
20
+-- column for grace-period restore).
21
+SELECT * FROM orgs
22
+WHERE slug = $1
23
+  AND deleted_at IS NULL;
24
+
25
+-- name: GetOrgBySlugIncludingDeleted :one
26
+SELECT * FROM orgs WHERE slug = $1;
27
+
28
+-- name: UpdateOrgProfile :exec
29
+UPDATE orgs
30
+   SET display_name = $2,
31
+       description  = $3,
32
+       location     = $4,
33
+       website      = $5,
34
+       billing_email = $6,
35
+       updated_at   = now()
36
+ WHERE id = $1;
37
+
38
+-- name: SetOrgAvatarKey :exec
39
+UPDATE orgs SET avatar_object_key = $2, updated_at = now() WHERE id = $1;
40
+
41
+-- name: SetOrgAllowMemberRepoCreate :exec
42
+UPDATE orgs SET allow_member_repo_create = $2, updated_at = now() WHERE id = $1;
43
+
44
+-- name: SetOrgSuspended :exec
45
+UPDATE orgs
46
+   SET suspended_at = CASE WHEN $2::boolean THEN now() ELSE NULL END,
47
+       suspended_reason = CASE WHEN $2::boolean THEN $3 ELSE NULL END,
48
+       updated_at = now()
49
+ WHERE id = $1;
50
+
51
+-- name: SoftDeleteOrg :exec
52
+UPDATE orgs SET deleted_at = now(), updated_at = now() WHERE id = $1;
53
+
54
+-- name: RestoreOrg :exec
55
+UPDATE orgs SET deleted_at = NULL, updated_at = now() WHERE id = $1;
56
+
57
+-- ─── principals (read-only from this domain) ───────────────────────
58
+
59
+-- name: ResolvePrincipal :one
60
+-- Single-query /{slug} resolver. Returns the (kind, id) tuple that
61
+-- /{slug}/* routes use to dispatch to the user-profile or org-profile
62
+-- handler. Both kinds share the slug PK so collisions are
63
+-- impossible at the DB layer.
64
+SELECT slug, kind, id FROM principals WHERE slug = $1;
internal/orgs/sqlc/db.goadded
@@ -0,0 +1,25 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+
5
+package orgsdb
6
+
7
+import (
8
+	"context"
9
+
10
+	"github.com/jackc/pgx/v5"
11
+	"github.com/jackc/pgx/v5/pgconn"
12
+)
13
+
14
+type DBTX interface {
15
+	Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
16
+	Query(context.Context, string, ...interface{}) (pgx.Rows, error)
17
+	QueryRow(context.Context, string, ...interface{}) pgx.Row
18
+}
19
+
20
+func New() *Queries {
21
+	return &Queries{}
22
+}
23
+
24
+type Queries struct {
25
+}
internal/orgs/sqlc/invitations.sql.goadded
@@ -0,0 +1,378 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+// source: invitations.sql
5
+
6
+package orgsdb
7
+
8
+import (
9
+	"context"
10
+
11
+	"github.com/jackc/pgx/v5/pgtype"
12
+)
13
+
14
+const acceptOrgInvitation = `-- name: AcceptOrgInvitation :exec
15
+UPDATE org_invitations SET accepted_at = now() WHERE id = $1
16
+`
17
+
18
+func (q *Queries) AcceptOrgInvitation(ctx context.Context, db DBTX, id int64) error {
19
+	_, err := db.Exec(ctx, acceptOrgInvitation, id)
20
+	return err
21
+}
22
+
23
+const cancelOrgInvitation = `-- name: CancelOrgInvitation :exec
24
+UPDATE org_invitations SET canceled_at = now() WHERE id = $1
25
+`
26
+
27
+func (q *Queries) CancelOrgInvitation(ctx context.Context, db DBTX, id int64) error {
28
+	_, err := db.Exec(ctx, cancelOrgInvitation, id)
29
+	return err
30
+}
31
+
32
+const createOrgInvitation = `-- name: CreateOrgInvitation :one
33
+
34
+
35
+INSERT INTO org_invitations (
36
+    org_id, invited_by_user_id, target_user_id, target_email,
37
+    role, token_hash, expires_at
38
+) VALUES (
39
+    $1, $2, $6::bigint, $7::citext,
40
+    $3, $4, $5
41
+)
42
+RETURNING id, org_id, invited_by_user_id, target_user_id, target_email, role, token_hash, expires_at, accepted_at, declined_at, canceled_at, created_at
43
+`
44
+
45
+type CreateOrgInvitationParams struct {
46
+	OrgID           int64
47
+	InvitedByUserID pgtype.Int8
48
+	Role            OrgRole
49
+	TokenHash       []byte
50
+	ExpiresAt       pgtype.Timestamptz
51
+	TargetUserID    pgtype.Int8
52
+	TargetEmail     pgtype.Text
53
+}
54
+
55
+// SPDX-License-Identifier: AGPL-3.0-or-later
56
+// ─── org_invitations ───────────────────────────────────────────────
57
+func (q *Queries) CreateOrgInvitation(ctx context.Context, db DBTX, arg CreateOrgInvitationParams) (OrgInvitation, error) {
58
+	row := db.QueryRow(ctx, createOrgInvitation,
59
+		arg.OrgID,
60
+		arg.InvitedByUserID,
61
+		arg.Role,
62
+		arg.TokenHash,
63
+		arg.ExpiresAt,
64
+		arg.TargetUserID,
65
+		arg.TargetEmail,
66
+	)
67
+	var i OrgInvitation
68
+	err := row.Scan(
69
+		&i.ID,
70
+		&i.OrgID,
71
+		&i.InvitedByUserID,
72
+		&i.TargetUserID,
73
+		&i.TargetEmail,
74
+		&i.Role,
75
+		&i.TokenHash,
76
+		&i.ExpiresAt,
77
+		&i.AcceptedAt,
78
+		&i.DeclinedAt,
79
+		&i.CanceledAt,
80
+		&i.CreatedAt,
81
+	)
82
+	return i, err
83
+}
84
+
85
+const declineOrgInvitation = `-- name: DeclineOrgInvitation :exec
86
+UPDATE org_invitations SET declined_at = now() WHERE id = $1
87
+`
88
+
89
+func (q *Queries) DeclineOrgInvitation(ctx context.Context, db DBTX, id int64) error {
90
+	_, err := db.Exec(ctx, declineOrgInvitation, id)
91
+	return err
92
+}
93
+
94
+const getExistingPendingInvitation = `-- name: GetExistingPendingInvitation :one
95
+SELECT id, org_id, invited_by_user_id, target_user_id, target_email, role, token_hash, expires_at, accepted_at, declined_at, canceled_at, created_at FROM org_invitations
96
+WHERE org_id = $1
97
+  AND ( (target_user_id = $2::bigint)
98
+     OR (target_email   = $3::citext) )
99
+  AND accepted_at IS NULL
100
+  AND declined_at IS NULL
101
+  AND canceled_at IS NULL
102
+  AND expires_at > now()
103
+LIMIT 1
104
+`
105
+
106
+type GetExistingPendingInvitationParams struct {
107
+	OrgID        int64
108
+	TargetUserID pgtype.Int8
109
+	TargetEmail  pgtype.Text
110
+}
111
+
112
+// Idempotency check before creating a new invite — so a re-issued
113
+// invite to the same target doesn't accumulate stale rows.
114
+func (q *Queries) GetExistingPendingInvitation(ctx context.Context, db DBTX, arg GetExistingPendingInvitationParams) (OrgInvitation, error) {
115
+	row := db.QueryRow(ctx, getExistingPendingInvitation, arg.OrgID, arg.TargetUserID, arg.TargetEmail)
116
+	var i OrgInvitation
117
+	err := row.Scan(
118
+		&i.ID,
119
+		&i.OrgID,
120
+		&i.InvitedByUserID,
121
+		&i.TargetUserID,
122
+		&i.TargetEmail,
123
+		&i.Role,
124
+		&i.TokenHash,
125
+		&i.ExpiresAt,
126
+		&i.AcceptedAt,
127
+		&i.DeclinedAt,
128
+		&i.CanceledAt,
129
+		&i.CreatedAt,
130
+	)
131
+	return i, err
132
+}
133
+
134
+const getOrgInvitationByID = `-- name: GetOrgInvitationByID :one
135
+SELECT id, org_id, invited_by_user_id, target_user_id, target_email, role, token_hash, expires_at, accepted_at, declined_at, canceled_at, created_at FROM org_invitations WHERE id = $1
136
+`
137
+
138
+func (q *Queries) GetOrgInvitationByID(ctx context.Context, db DBTX, id int64) (OrgInvitation, error) {
139
+	row := db.QueryRow(ctx, getOrgInvitationByID, id)
140
+	var i OrgInvitation
141
+	err := row.Scan(
142
+		&i.ID,
143
+		&i.OrgID,
144
+		&i.InvitedByUserID,
145
+		&i.TargetUserID,
146
+		&i.TargetEmail,
147
+		&i.Role,
148
+		&i.TokenHash,
149
+		&i.ExpiresAt,
150
+		&i.AcceptedAt,
151
+		&i.DeclinedAt,
152
+		&i.CanceledAt,
153
+		&i.CreatedAt,
154
+	)
155
+	return i, err
156
+}
157
+
158
+const getOrgInvitationByTokenHash = `-- name: GetOrgInvitationByTokenHash :one
159
+SELECT id, org_id, invited_by_user_id, target_user_id, target_email, role, token_hash, expires_at, accepted_at, declined_at, canceled_at, created_at FROM org_invitations WHERE token_hash = $1
160
+`
161
+
162
+func (q *Queries) GetOrgInvitationByTokenHash(ctx context.Context, db DBTX, tokenHash []byte) (OrgInvitation, error) {
163
+	row := db.QueryRow(ctx, getOrgInvitationByTokenHash, tokenHash)
164
+	var i OrgInvitation
165
+	err := row.Scan(
166
+		&i.ID,
167
+		&i.OrgID,
168
+		&i.InvitedByUserID,
169
+		&i.TargetUserID,
170
+		&i.TargetEmail,
171
+		&i.Role,
172
+		&i.TokenHash,
173
+		&i.ExpiresAt,
174
+		&i.AcceptedAt,
175
+		&i.DeclinedAt,
176
+		&i.CanceledAt,
177
+		&i.CreatedAt,
178
+	)
179
+	return i, err
180
+}
181
+
182
+const listPendingInvitationsForEmail = `-- name: ListPendingInvitationsForEmail :many
183
+SELECT i.id, i.org_id, i.invited_by_user_id, i.target_user_id, i.target_email, i.role, i.token_hash, i.expires_at, i.accepted_at, i.declined_at, i.canceled_at, i.created_at, o.slug AS org_slug, o.display_name AS org_display_name
184
+FROM org_invitations i
185
+JOIN orgs o ON o.id = i.org_id
186
+WHERE i.target_email = $1
187
+  AND i.accepted_at IS NULL
188
+  AND i.declined_at IS NULL
189
+  AND i.canceled_at IS NULL
190
+  AND i.expires_at > now()
191
+  AND o.deleted_at IS NULL
192
+ORDER BY i.created_at DESC
193
+`
194
+
195
+type ListPendingInvitationsForEmailRow struct {
196
+	ID              int64
197
+	OrgID           int64
198
+	InvitedByUserID pgtype.Int8
199
+	TargetUserID    pgtype.Int8
200
+	TargetEmail     pgtype.Text
201
+	Role            OrgRole
202
+	TokenHash       []byte
203
+	ExpiresAt       pgtype.Timestamptz
204
+	AcceptedAt      pgtype.Timestamptz
205
+	DeclinedAt      pgtype.Timestamptz
206
+	CanceledAt      pgtype.Timestamptz
207
+	CreatedAt       pgtype.Timestamptz
208
+	OrgSlug         string
209
+	OrgDisplayName  string
210
+}
211
+
212
+func (q *Queries) ListPendingInvitationsForEmail(ctx context.Context, db DBTX, targetEmail pgtype.Text) ([]ListPendingInvitationsForEmailRow, error) {
213
+	rows, err := db.Query(ctx, listPendingInvitationsForEmail, targetEmail)
214
+	if err != nil {
215
+		return nil, err
216
+	}
217
+	defer rows.Close()
218
+	items := []ListPendingInvitationsForEmailRow{}
219
+	for rows.Next() {
220
+		var i ListPendingInvitationsForEmailRow
221
+		if err := rows.Scan(
222
+			&i.ID,
223
+			&i.OrgID,
224
+			&i.InvitedByUserID,
225
+			&i.TargetUserID,
226
+			&i.TargetEmail,
227
+			&i.Role,
228
+			&i.TokenHash,
229
+			&i.ExpiresAt,
230
+			&i.AcceptedAt,
231
+			&i.DeclinedAt,
232
+			&i.CanceledAt,
233
+			&i.CreatedAt,
234
+			&i.OrgSlug,
235
+			&i.OrgDisplayName,
236
+		); err != nil {
237
+			return nil, err
238
+		}
239
+		items = append(items, i)
240
+	}
241
+	if err := rows.Err(); err != nil {
242
+		return nil, err
243
+	}
244
+	return items, nil
245
+}
246
+
247
+const listPendingInvitationsForOrg = `-- name: ListPendingInvitationsForOrg :many
248
+SELECT i.id, i.org_id, i.invited_by_user_id, i.target_user_id, i.target_email, i.role, i.token_hash, i.expires_at, i.accepted_at, i.declined_at, i.canceled_at, i.created_at, u.username AS target_username, ib.username AS invited_by_username
249
+FROM org_invitations i
250
+LEFT JOIN users u  ON u.id  = i.target_user_id
251
+LEFT JOIN users ib ON ib.id = i.invited_by_user_id
252
+WHERE i.org_id = $1
253
+  AND i.accepted_at IS NULL
254
+  AND i.declined_at IS NULL
255
+  AND i.canceled_at IS NULL
256
+  AND i.expires_at > now()
257
+ORDER BY i.created_at DESC
258
+`
259
+
260
+type ListPendingInvitationsForOrgRow struct {
261
+	ID                int64
262
+	OrgID             int64
263
+	InvitedByUserID   pgtype.Int8
264
+	TargetUserID      pgtype.Int8
265
+	TargetEmail       pgtype.Text
266
+	Role              OrgRole
267
+	TokenHash         []byte
268
+	ExpiresAt         pgtype.Timestamptz
269
+	AcceptedAt        pgtype.Timestamptz
270
+	DeclinedAt        pgtype.Timestamptz
271
+	CanceledAt        pgtype.Timestamptz
272
+	CreatedAt         pgtype.Timestamptz
273
+	TargetUsername    pgtype.Text
274
+	InvitedByUsername pgtype.Text
275
+}
276
+
277
+func (q *Queries) ListPendingInvitationsForOrg(ctx context.Context, db DBTX, orgID int64) ([]ListPendingInvitationsForOrgRow, error) {
278
+	rows, err := db.Query(ctx, listPendingInvitationsForOrg, orgID)
279
+	if err != nil {
280
+		return nil, err
281
+	}
282
+	defer rows.Close()
283
+	items := []ListPendingInvitationsForOrgRow{}
284
+	for rows.Next() {
285
+		var i ListPendingInvitationsForOrgRow
286
+		if err := rows.Scan(
287
+			&i.ID,
288
+			&i.OrgID,
289
+			&i.InvitedByUserID,
290
+			&i.TargetUserID,
291
+			&i.TargetEmail,
292
+			&i.Role,
293
+			&i.TokenHash,
294
+			&i.ExpiresAt,
295
+			&i.AcceptedAt,
296
+			&i.DeclinedAt,
297
+			&i.CanceledAt,
298
+			&i.CreatedAt,
299
+			&i.TargetUsername,
300
+			&i.InvitedByUsername,
301
+		); err != nil {
302
+			return nil, err
303
+		}
304
+		items = append(items, i)
305
+	}
306
+	if err := rows.Err(); err != nil {
307
+		return nil, err
308
+	}
309
+	return items, nil
310
+}
311
+
312
+const listPendingInvitationsForUser = `-- name: ListPendingInvitationsForUser :many
313
+SELECT i.id, i.org_id, i.invited_by_user_id, i.target_user_id, i.target_email, i.role, i.token_hash, i.expires_at, i.accepted_at, i.declined_at, i.canceled_at, i.created_at, o.slug AS org_slug, o.display_name AS org_display_name
314
+FROM org_invitations i
315
+JOIN orgs o ON o.id = i.org_id
316
+WHERE i.target_user_id = $1
317
+  AND i.accepted_at IS NULL
318
+  AND i.declined_at IS NULL
319
+  AND i.canceled_at IS NULL
320
+  AND i.expires_at > now()
321
+  AND o.deleted_at IS NULL
322
+ORDER BY i.created_at DESC
323
+`
324
+
325
+type ListPendingInvitationsForUserRow struct {
326
+	ID              int64
327
+	OrgID           int64
328
+	InvitedByUserID pgtype.Int8
329
+	TargetUserID    pgtype.Int8
330
+	TargetEmail     pgtype.Text
331
+	Role            OrgRole
332
+	TokenHash       []byte
333
+	ExpiresAt       pgtype.Timestamptz
334
+	AcceptedAt      pgtype.Timestamptz
335
+	DeclinedAt      pgtype.Timestamptz
336
+	CanceledAt      pgtype.Timestamptz
337
+	CreatedAt       pgtype.Timestamptz
338
+	OrgSlug         string
339
+	OrgDisplayName  string
340
+}
341
+
342
+// Two flavors: by user_id (already-claimed invites) and by email
343
+// (claim-on-signup). Caller unions the two lookups when surfacing
344
+// to the user.
345
+func (q *Queries) ListPendingInvitationsForUser(ctx context.Context, db DBTX, targetUserID pgtype.Int8) ([]ListPendingInvitationsForUserRow, error) {
346
+	rows, err := db.Query(ctx, listPendingInvitationsForUser, targetUserID)
347
+	if err != nil {
348
+		return nil, err
349
+	}
350
+	defer rows.Close()
351
+	items := []ListPendingInvitationsForUserRow{}
352
+	for rows.Next() {
353
+		var i ListPendingInvitationsForUserRow
354
+		if err := rows.Scan(
355
+			&i.ID,
356
+			&i.OrgID,
357
+			&i.InvitedByUserID,
358
+			&i.TargetUserID,
359
+			&i.TargetEmail,
360
+			&i.Role,
361
+			&i.TokenHash,
362
+			&i.ExpiresAt,
363
+			&i.AcceptedAt,
364
+			&i.DeclinedAt,
365
+			&i.CanceledAt,
366
+			&i.CreatedAt,
367
+			&i.OrgSlug,
368
+			&i.OrgDisplayName,
369
+		); err != nil {
370
+			return nil, err
371
+		}
372
+		items = append(items, i)
373
+	}
374
+	if err := rows.Err(); err != nil {
375
+		return nil, err
376
+	}
377
+	return items, nil
378
+}
internal/orgs/sqlc/members.sql.goadded
@@ -0,0 +1,200 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+// source: members.sql
5
+
6
+package orgsdb
7
+
8
+import (
9
+	"context"
10
+
11
+	"github.com/jackc/pgx/v5/pgtype"
12
+)
13
+
14
+const addOrgMember = `-- name: AddOrgMember :exec
15
+
16
+
17
+INSERT INTO org_members (org_id, user_id, role, invited_by_user_id)
18
+VALUES ($1, $2, $3, $4::bigint)
19
+ON CONFLICT (org_id, user_id) DO NOTHING
20
+`
21
+
22
+type AddOrgMemberParams struct {
23
+	OrgID           int64
24
+	UserID          int64
25
+	Role            OrgRole
26
+	InvitedByUserID pgtype.Int8
27
+}
28
+
29
+// SPDX-License-Identifier: AGPL-3.0-or-later
30
+// ─── org_members ───────────────────────────────────────────────────
31
+// Idempotent on (org_id, user_id): re-adding a member is a no-op
32
+// rather than an error. The role is taken from the supplied row when
33
+// the member is new; existing rows keep their current role (use
34
+// ChangeOrgMemberRole to update).
35
+func (q *Queries) AddOrgMember(ctx context.Context, db DBTX, arg AddOrgMemberParams) error {
36
+	_, err := db.Exec(ctx, addOrgMember,
37
+		arg.OrgID,
38
+		arg.UserID,
39
+		arg.Role,
40
+		arg.InvitedByUserID,
41
+	)
42
+	return err
43
+}
44
+
45
+const changeOrgMemberRole = `-- name: ChangeOrgMemberRole :exec
46
+UPDATE org_members SET role = $3 WHERE org_id = $1 AND user_id = $2
47
+`
48
+
49
+type ChangeOrgMemberRoleParams struct {
50
+	OrgID  int64
51
+	UserID int64
52
+	Role   OrgRole
53
+}
54
+
55
+func (q *Queries) ChangeOrgMemberRole(ctx context.Context, db DBTX, arg ChangeOrgMemberRoleParams) error {
56
+	_, err := db.Exec(ctx, changeOrgMemberRole, arg.OrgID, arg.UserID, arg.Role)
57
+	return err
58
+}
59
+
60
+const countOrgOwners = `-- name: CountOrgOwners :one
61
+SELECT count(*) FROM org_members WHERE org_id = $1 AND role = 'owner'
62
+`
63
+
64
+// Used by the last-owner protection: refuses to remove or demote the
65
+// only owner. Caller compares `count = 1` before allowing the change.
66
+func (q *Queries) CountOrgOwners(ctx context.Context, db DBTX, orgID int64) (int64, error) {
67
+	row := db.QueryRow(ctx, countOrgOwners, orgID)
68
+	var count int64
69
+	err := row.Scan(&count)
70
+	return count, err
71
+}
72
+
73
+const getOrgMember = `-- name: GetOrgMember :one
74
+SELECT org_id, user_id, role, invited_by_user_id, joined_at FROM org_members WHERE org_id = $1 AND user_id = $2
75
+`
76
+
77
+type GetOrgMemberParams struct {
78
+	OrgID  int64
79
+	UserID int64
80
+}
81
+
82
+func (q *Queries) GetOrgMember(ctx context.Context, db DBTX, arg GetOrgMemberParams) (OrgMember, error) {
83
+	row := db.QueryRow(ctx, getOrgMember, arg.OrgID, arg.UserID)
84
+	var i OrgMember
85
+	err := row.Scan(
86
+		&i.OrgID,
87
+		&i.UserID,
88
+		&i.Role,
89
+		&i.InvitedByUserID,
90
+		&i.JoinedAt,
91
+	)
92
+	return i, err
93
+}
94
+
95
+const listOrgMembers = `-- name: ListOrgMembers :many
96
+SELECT m.org_id, m.user_id, m.role, m.invited_by_user_id, m.joined_at,
97
+       u.username, u.display_name
98
+FROM org_members m
99
+JOIN users u ON u.id = m.user_id
100
+WHERE m.org_id = $1
101
+  AND u.deleted_at IS NULL
102
+ORDER BY m.role ASC, u.username ASC
103
+`
104
+
105
+type ListOrgMembersRow struct {
106
+	OrgID           int64
107
+	UserID          int64
108
+	Role            OrgRole
109
+	InvitedByUserID pgtype.Int8
110
+	JoinedAt        pgtype.Timestamptz
111
+	Username        string
112
+	DisplayName     string
113
+}
114
+
115
+// Members of an org with usernames + roles for the people page.
116
+func (q *Queries) ListOrgMembers(ctx context.Context, db DBTX, orgID int64) ([]ListOrgMembersRow, error) {
117
+	rows, err := db.Query(ctx, listOrgMembers, orgID)
118
+	if err != nil {
119
+		return nil, err
120
+	}
121
+	defer rows.Close()
122
+	items := []ListOrgMembersRow{}
123
+	for rows.Next() {
124
+		var i ListOrgMembersRow
125
+		if err := rows.Scan(
126
+			&i.OrgID,
127
+			&i.UserID,
128
+			&i.Role,
129
+			&i.InvitedByUserID,
130
+			&i.JoinedAt,
131
+			&i.Username,
132
+			&i.DisplayName,
133
+		); err != nil {
134
+			return nil, err
135
+		}
136
+		items = append(items, i)
137
+	}
138
+	if err := rows.Err(); err != nil {
139
+		return nil, err
140
+	}
141
+	return items, nil
142
+}
143
+
144
+const listOrgsForUser = `-- name: ListOrgsForUser :many
145
+SELECT m.org_id, m.role, o.slug, o.display_name, o.avatar_object_key
146
+FROM org_members m
147
+JOIN orgs o ON o.id = m.org_id
148
+WHERE m.user_id = $1
149
+  AND o.deleted_at IS NULL
150
+ORDER BY o.slug ASC
151
+`
152
+
153
+type ListOrgsForUserRow struct {
154
+	OrgID           int64
155
+	Role            OrgRole
156
+	Slug            string
157
+	DisplayName     string
158
+	AvatarObjectKey pgtype.Text
159
+}
160
+
161
+// Profile-page input: every org a user is a member of, with role.
162
+func (q *Queries) ListOrgsForUser(ctx context.Context, db DBTX, userID int64) ([]ListOrgsForUserRow, error) {
163
+	rows, err := db.Query(ctx, listOrgsForUser, userID)
164
+	if err != nil {
165
+		return nil, err
166
+	}
167
+	defer rows.Close()
168
+	items := []ListOrgsForUserRow{}
169
+	for rows.Next() {
170
+		var i ListOrgsForUserRow
171
+		if err := rows.Scan(
172
+			&i.OrgID,
173
+			&i.Role,
174
+			&i.Slug,
175
+			&i.DisplayName,
176
+			&i.AvatarObjectKey,
177
+		); err != nil {
178
+			return nil, err
179
+		}
180
+		items = append(items, i)
181
+	}
182
+	if err := rows.Err(); err != nil {
183
+		return nil, err
184
+	}
185
+	return items, nil
186
+}
187
+
188
+const removeOrgMember = `-- name: RemoveOrgMember :exec
189
+DELETE FROM org_members WHERE org_id = $1 AND user_id = $2
190
+`
191
+
192
+type RemoveOrgMemberParams struct {
193
+	OrgID  int64
194
+	UserID int64
195
+}
196
+
197
+func (q *Queries) RemoveOrgMember(ctx context.Context, db DBTX, arg RemoveOrgMemberParams) error {
198
+	_, err := db.Exec(ctx, removeOrgMember, arg.OrgID, arg.UserID)
199
+	return err
200
+}
internal/notif/sqlc/models.go → internal/orgs/sqlc/models.gocopied (89% similarity)
@@ -2,7 +2,7 @@
22
 // versions:
33
 //   sqlc v1.31.1
44
 
5
-package notifdb
5
+package orgsdb
66
 
77
 import (
88
 	"database/sql/driver"
@@ -404,6 +404,91 @@ func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
404404
 	return string(ns.NotificationThreadKind), nil
405405
 }
406406
 
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
407492
 type PrFileStatus string
408493
 
409494
 const (
@@ -622,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
622707
 	return string(ns.PrReviewState), nil
623708
 }
624709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
625752
 type RepoInitStatus string
626753
 
627754
 const (
@@ -1097,6 +1224,48 @@ type NotificationThread struct {
10971224
 	UpdatedAt       pgtype.Timestamptz
10981225
 }
10991226
 
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
11001269
 type PasswordReset struct {
11011270
 	ID        int64
11021271
 	UserID    int64
@@ -1152,6 +1321,12 @@ type PrReviewRequest struct {
11521321
 	SatisfiedByReviewID pgtype.Int8
11531322
 }
11541323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
11551330
 type PullRequest struct {
11561331
 	IssueID            int64
11571332
 	BaseRef            string
internal/orgs/sqlc/orgs.sql.goadded
@@ -0,0 +1,267 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+// source: orgs.sql
5
+
6
+package orgsdb
7
+
8
+import (
9
+	"context"
10
+
11
+	"github.com/jackc/pgx/v5/pgtype"
12
+)
13
+
14
+const createOrg = `-- name: CreateOrg :one
15
+
16
+
17
+INSERT INTO orgs (slug, display_name, description, billing_email, created_by_user_id)
18
+VALUES ($1, $2, $3, $4, $5::bigint)
19
+RETURNING id, slug, display_name, description, avatar_object_key, location, website, billing_email, plan, allow_member_repo_create, created_by_user_id, suspended_at, suspended_reason, deleted_at, created_at, updated_at
20
+`
21
+
22
+type CreateOrgParams struct {
23
+	Slug            string
24
+	DisplayName     string
25
+	Description     string
26
+	BillingEmail    string
27
+	CreatedByUserID pgtype.Int8
28
+}
29
+
30
+// SPDX-License-Identifier: AGPL-3.0-or-later
31
+// ─── orgs ──────────────────────────────────────────────────────────
32
+// Inserts a new org. The trigger on `orgs` populates `principals` for
33
+// /{slug} resolution; the caller adds the creator as owner in the same
34
+// tx (separate query so the orchestrator owns ordering).
35
+func (q *Queries) CreateOrg(ctx context.Context, db DBTX, arg CreateOrgParams) (Org, error) {
36
+	row := db.QueryRow(ctx, createOrg,
37
+		arg.Slug,
38
+		arg.DisplayName,
39
+		arg.Description,
40
+		arg.BillingEmail,
41
+		arg.CreatedByUserID,
42
+	)
43
+	var i Org
44
+	err := row.Scan(
45
+		&i.ID,
46
+		&i.Slug,
47
+		&i.DisplayName,
48
+		&i.Description,
49
+		&i.AvatarObjectKey,
50
+		&i.Location,
51
+		&i.Website,
52
+		&i.BillingEmail,
53
+		&i.Plan,
54
+		&i.AllowMemberRepoCreate,
55
+		&i.CreatedByUserID,
56
+		&i.SuspendedAt,
57
+		&i.SuspendedReason,
58
+		&i.DeletedAt,
59
+		&i.CreatedAt,
60
+		&i.UpdatedAt,
61
+	)
62
+	return i, err
63
+}
64
+
65
+const getOrgByID = `-- name: GetOrgByID :one
66
+SELECT id, slug, display_name, description, avatar_object_key, location, website, billing_email, plan, allow_member_repo_create, created_by_user_id, suspended_at, suspended_reason, deleted_at, created_at, updated_at FROM orgs WHERE id = $1
67
+`
68
+
69
+func (q *Queries) GetOrgByID(ctx context.Context, db DBTX, id int64) (Org, error) {
70
+	row := db.QueryRow(ctx, getOrgByID, id)
71
+	var i Org
72
+	err := row.Scan(
73
+		&i.ID,
74
+		&i.Slug,
75
+		&i.DisplayName,
76
+		&i.Description,
77
+		&i.AvatarObjectKey,
78
+		&i.Location,
79
+		&i.Website,
80
+		&i.BillingEmail,
81
+		&i.Plan,
82
+		&i.AllowMemberRepoCreate,
83
+		&i.CreatedByUserID,
84
+		&i.SuspendedAt,
85
+		&i.SuspendedReason,
86
+		&i.DeletedAt,
87
+		&i.CreatedAt,
88
+		&i.UpdatedAt,
89
+	)
90
+	return i, err
91
+}
92
+
93
+const getOrgBySlug = `-- name: GetOrgBySlug :one
94
+SELECT id, slug, display_name, description, avatar_object_key, location, website, billing_email, plan, allow_member_repo_create, created_by_user_id, suspended_at, suspended_reason, deleted_at, created_at, updated_at FROM orgs
95
+WHERE slug = $1
96
+  AND deleted_at IS NULL
97
+`
98
+
99
+// Slug is citext so the comparison is case-insensitive. Soft-deleted
100
+// orgs are filtered out so a slug freed by deletion is invisible to
101
+// normal lookups (the resolver still sees the row via the deleted_at
102
+// column for grace-period restore).
103
+func (q *Queries) GetOrgBySlug(ctx context.Context, db DBTX, slug string) (Org, error) {
104
+	row := db.QueryRow(ctx, getOrgBySlug, slug)
105
+	var i Org
106
+	err := row.Scan(
107
+		&i.ID,
108
+		&i.Slug,
109
+		&i.DisplayName,
110
+		&i.Description,
111
+		&i.AvatarObjectKey,
112
+		&i.Location,
113
+		&i.Website,
114
+		&i.BillingEmail,
115
+		&i.Plan,
116
+		&i.AllowMemberRepoCreate,
117
+		&i.CreatedByUserID,
118
+		&i.SuspendedAt,
119
+		&i.SuspendedReason,
120
+		&i.DeletedAt,
121
+		&i.CreatedAt,
122
+		&i.UpdatedAt,
123
+	)
124
+	return i, err
125
+}
126
+
127
+const getOrgBySlugIncludingDeleted = `-- name: GetOrgBySlugIncludingDeleted :one
128
+SELECT id, slug, display_name, description, avatar_object_key, location, website, billing_email, plan, allow_member_repo_create, created_by_user_id, suspended_at, suspended_reason, deleted_at, created_at, updated_at FROM orgs WHERE slug = $1
129
+`
130
+
131
+func (q *Queries) GetOrgBySlugIncludingDeleted(ctx context.Context, db DBTX, slug string) (Org, error) {
132
+	row := db.QueryRow(ctx, getOrgBySlugIncludingDeleted, slug)
133
+	var i Org
134
+	err := row.Scan(
135
+		&i.ID,
136
+		&i.Slug,
137
+		&i.DisplayName,
138
+		&i.Description,
139
+		&i.AvatarObjectKey,
140
+		&i.Location,
141
+		&i.Website,
142
+		&i.BillingEmail,
143
+		&i.Plan,
144
+		&i.AllowMemberRepoCreate,
145
+		&i.CreatedByUserID,
146
+		&i.SuspendedAt,
147
+		&i.SuspendedReason,
148
+		&i.DeletedAt,
149
+		&i.CreatedAt,
150
+		&i.UpdatedAt,
151
+	)
152
+	return i, err
153
+}
154
+
155
+const resolvePrincipal = `-- name: ResolvePrincipal :one
156
+
157
+SELECT slug, kind, id FROM principals WHERE slug = $1
158
+`
159
+
160
+// ─── principals (read-only from this domain) ───────────────────────
161
+// Single-query /{slug} resolver. Returns the (kind, id) tuple that
162
+// /{slug}/* routes use to dispatch to the user-profile or org-profile
163
+// handler. Both kinds share the slug PK so collisions are
164
+// impossible at the DB layer.
165
+func (q *Queries) ResolvePrincipal(ctx context.Context, db DBTX, slug string) (Principal, error) {
166
+	row := db.QueryRow(ctx, resolvePrincipal, slug)
167
+	var i Principal
168
+	err := row.Scan(&i.Slug, &i.Kind, &i.ID)
169
+	return i, err
170
+}
171
+
172
+const restoreOrg = `-- name: RestoreOrg :exec
173
+UPDATE orgs SET deleted_at = NULL, updated_at = now() WHERE id = $1
174
+`
175
+
176
+func (q *Queries) RestoreOrg(ctx context.Context, db DBTX, id int64) error {
177
+	_, err := db.Exec(ctx, restoreOrg, id)
178
+	return err
179
+}
180
+
181
+const setOrgAllowMemberRepoCreate = `-- name: SetOrgAllowMemberRepoCreate :exec
182
+UPDATE orgs SET allow_member_repo_create = $2, updated_at = now() WHERE id = $1
183
+`
184
+
185
+type SetOrgAllowMemberRepoCreateParams struct {
186
+	ID                    int64
187
+	AllowMemberRepoCreate bool
188
+}
189
+
190
+func (q *Queries) SetOrgAllowMemberRepoCreate(ctx context.Context, db DBTX, arg SetOrgAllowMemberRepoCreateParams) error {
191
+	_, err := db.Exec(ctx, setOrgAllowMemberRepoCreate, arg.ID, arg.AllowMemberRepoCreate)
192
+	return err
193
+}
194
+
195
+const setOrgAvatarKey = `-- name: SetOrgAvatarKey :exec
196
+UPDATE orgs SET avatar_object_key = $2, updated_at = now() WHERE id = $1
197
+`
198
+
199
+type SetOrgAvatarKeyParams struct {
200
+	ID              int64
201
+	AvatarObjectKey pgtype.Text
202
+}
203
+
204
+func (q *Queries) SetOrgAvatarKey(ctx context.Context, db DBTX, arg SetOrgAvatarKeyParams) error {
205
+	_, err := db.Exec(ctx, setOrgAvatarKey, arg.ID, arg.AvatarObjectKey)
206
+	return err
207
+}
208
+
209
+const setOrgSuspended = `-- name: SetOrgSuspended :exec
210
+UPDATE orgs
211
+   SET suspended_at = CASE WHEN $2::boolean THEN now() ELSE NULL END,
212
+       suspended_reason = CASE WHEN $2::boolean THEN $3 ELSE NULL END,
213
+       updated_at = now()
214
+ WHERE id = $1
215
+`
216
+
217
+type SetOrgSuspendedParams struct {
218
+	ID              int64
219
+	Column2         bool
220
+	SuspendedReason pgtype.Text
221
+}
222
+
223
+func (q *Queries) SetOrgSuspended(ctx context.Context, db DBTX, arg SetOrgSuspendedParams) error {
224
+	_, err := db.Exec(ctx, setOrgSuspended, arg.ID, arg.Column2, arg.SuspendedReason)
225
+	return err
226
+}
227
+
228
+const softDeleteOrg = `-- name: SoftDeleteOrg :exec
229
+UPDATE orgs SET deleted_at = now(), updated_at = now() WHERE id = $1
230
+`
231
+
232
+func (q *Queries) SoftDeleteOrg(ctx context.Context, db DBTX, id int64) error {
233
+	_, err := db.Exec(ctx, softDeleteOrg, id)
234
+	return err
235
+}
236
+
237
+const updateOrgProfile = `-- name: UpdateOrgProfile :exec
238
+UPDATE orgs
239
+   SET display_name = $2,
240
+       description  = $3,
241
+       location     = $4,
242
+       website      = $5,
243
+       billing_email = $6,
244
+       updated_at   = now()
245
+ WHERE id = $1
246
+`
247
+
248
+type UpdateOrgProfileParams struct {
249
+	ID           int64
250
+	DisplayName  string
251
+	Description  string
252
+	Location     string
253
+	Website      string
254
+	BillingEmail string
255
+}
256
+
257
+func (q *Queries) UpdateOrgProfile(ctx context.Context, db DBTX, arg UpdateOrgProfileParams) error {
258
+	_, err := db.Exec(ctx, updateOrgProfile,
259
+		arg.ID,
260
+		arg.DisplayName,
261
+		arg.Description,
262
+		arg.Location,
263
+		arg.Website,
264
+		arg.BillingEmail,
265
+	)
266
+	return err
267
+}
internal/orgs/sqlc/querier.goadded
@@ -0,0 +1,75 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+
5
+package orgsdb
6
+
7
+import (
8
+	"context"
9
+
10
+	"github.com/jackc/pgx/v5/pgtype"
11
+)
12
+
13
+type Querier interface {
14
+	AcceptOrgInvitation(ctx context.Context, db DBTX, id int64) error
15
+	// SPDX-License-Identifier: AGPL-3.0-or-later
16
+	// ─── org_members ───────────────────────────────────────────────────
17
+	// Idempotent on (org_id, user_id): re-adding a member is a no-op
18
+	// rather than an error. The role is taken from the supplied row when
19
+	// the member is new; existing rows keep their current role (use
20
+	// ChangeOrgMemberRole to update).
21
+	AddOrgMember(ctx context.Context, db DBTX, arg AddOrgMemberParams) error
22
+	CancelOrgInvitation(ctx context.Context, db DBTX, id int64) error
23
+	ChangeOrgMemberRole(ctx context.Context, db DBTX, arg ChangeOrgMemberRoleParams) error
24
+	// Used by the last-owner protection: refuses to remove or demote the
25
+	// only owner. Caller compares `count = 1` before allowing the change.
26
+	CountOrgOwners(ctx context.Context, db DBTX, orgID int64) (int64, error)
27
+	// SPDX-License-Identifier: AGPL-3.0-or-later
28
+	// ─── orgs ──────────────────────────────────────────────────────────
29
+	// Inserts a new org. The trigger on `orgs` populates `principals` for
30
+	// /{slug} resolution; the caller adds the creator as owner in the same
31
+	// tx (separate query so the orchestrator owns ordering).
32
+	CreateOrg(ctx context.Context, db DBTX, arg CreateOrgParams) (Org, error)
33
+	// SPDX-License-Identifier: AGPL-3.0-or-later
34
+	// ─── org_invitations ───────────────────────────────────────────────
35
+	CreateOrgInvitation(ctx context.Context, db DBTX, arg CreateOrgInvitationParams) (OrgInvitation, error)
36
+	DeclineOrgInvitation(ctx context.Context, db DBTX, id int64) error
37
+	// Idempotency check before creating a new invite — so a re-issued
38
+	// invite to the same target doesn't accumulate stale rows.
39
+	GetExistingPendingInvitation(ctx context.Context, db DBTX, arg GetExistingPendingInvitationParams) (OrgInvitation, error)
40
+	GetOrgByID(ctx context.Context, db DBTX, id int64) (Org, error)
41
+	// Slug is citext so the comparison is case-insensitive. Soft-deleted
42
+	// orgs are filtered out so a slug freed by deletion is invisible to
43
+	// normal lookups (the resolver still sees the row via the deleted_at
44
+	// column for grace-period restore).
45
+	GetOrgBySlug(ctx context.Context, db DBTX, slug string) (Org, error)
46
+	GetOrgBySlugIncludingDeleted(ctx context.Context, db DBTX, slug string) (Org, error)
47
+	GetOrgInvitationByID(ctx context.Context, db DBTX, id int64) (OrgInvitation, error)
48
+	GetOrgInvitationByTokenHash(ctx context.Context, db DBTX, tokenHash []byte) (OrgInvitation, error)
49
+	GetOrgMember(ctx context.Context, db DBTX, arg GetOrgMemberParams) (OrgMember, error)
50
+	// Members of an org with usernames + roles for the people page.
51
+	ListOrgMembers(ctx context.Context, db DBTX, orgID int64) ([]ListOrgMembersRow, error)
52
+	// Profile-page input: every org a user is a member of, with role.
53
+	ListOrgsForUser(ctx context.Context, db DBTX, userID int64) ([]ListOrgsForUserRow, error)
54
+	ListPendingInvitationsForEmail(ctx context.Context, db DBTX, targetEmail pgtype.Text) ([]ListPendingInvitationsForEmailRow, error)
55
+	ListPendingInvitationsForOrg(ctx context.Context, db DBTX, orgID int64) ([]ListPendingInvitationsForOrgRow, error)
56
+	// Two flavors: by user_id (already-claimed invites) and by email
57
+	// (claim-on-signup). Caller unions the two lookups when surfacing
58
+	// to the user.
59
+	ListPendingInvitationsForUser(ctx context.Context, db DBTX, targetUserID pgtype.Int8) ([]ListPendingInvitationsForUserRow, error)
60
+	RemoveOrgMember(ctx context.Context, db DBTX, arg RemoveOrgMemberParams) error
61
+	// ─── principals (read-only from this domain) ───────────────────────
62
+	// Single-query /{slug} resolver. Returns the (kind, id) tuple that
63
+	// /{slug}/* routes use to dispatch to the user-profile or org-profile
64
+	// handler. Both kinds share the slug PK so collisions are
65
+	// impossible at the DB layer.
66
+	ResolvePrincipal(ctx context.Context, db DBTX, slug string) (Principal, error)
67
+	RestoreOrg(ctx context.Context, db DBTX, id int64) error
68
+	SetOrgAllowMemberRepoCreate(ctx context.Context, db DBTX, arg SetOrgAllowMemberRepoCreateParams) error
69
+	SetOrgAvatarKey(ctx context.Context, db DBTX, arg SetOrgAvatarKeyParams) error
70
+	SetOrgSuspended(ctx context.Context, db DBTX, arg SetOrgSuspendedParams) error
71
+	SoftDeleteOrg(ctx context.Context, db DBTX, id int64) error
72
+	UpdateOrgProfile(ctx context.Context, db DBTX, arg UpdateOrgProfileParams) error
73
+}
74
+
75
+var _ Querier = (*Queries)(nil)
internal/pulls/sqlc/models.gomodified
@@ -362,6 +362,133 @@ func (ns NullMilestoneState) Value() (driver.Value, error) {
362362
 	return string(ns.MilestoneState), nil
363363
 }
364364
 
365
+type NotificationThreadKind string
366
+
367
+const (
368
+	NotificationThreadKindIssue NotificationThreadKind = "issue"
369
+	NotificationThreadKindPr    NotificationThreadKind = "pr"
370
+)
371
+
372
+func (e *NotificationThreadKind) Scan(src interface{}) error {
373
+	switch s := src.(type) {
374
+	case []byte:
375
+		*e = NotificationThreadKind(s)
376
+	case string:
377
+		*e = NotificationThreadKind(s)
378
+	default:
379
+		return fmt.Errorf("unsupported scan type for NotificationThreadKind: %T", src)
380
+	}
381
+	return nil
382
+}
383
+
384
+type NullNotificationThreadKind struct {
385
+	NotificationThreadKind NotificationThreadKind
386
+	Valid                  bool // Valid is true if NotificationThreadKind is not NULL
387
+}
388
+
389
+// Scan implements the Scanner interface.
390
+func (ns *NullNotificationThreadKind) Scan(value interface{}) error {
391
+	if value == nil {
392
+		ns.NotificationThreadKind, ns.Valid = "", false
393
+		return nil
394
+	}
395
+	ns.Valid = true
396
+	return ns.NotificationThreadKind.Scan(value)
397
+}
398
+
399
+// Value implements the driver Valuer interface.
400
+func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
401
+	if !ns.Valid {
402
+		return nil, nil
403
+	}
404
+	return string(ns.NotificationThreadKind), nil
405
+}
406
+
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
365492
 type PrFileStatus string
366493
 
367494
 const (
@@ -580,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
580707
 	return string(ns.PrReviewState), nil
581708
 }
582709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
583752
 type RepoInitStatus string
584753
 
585754
 const (
@@ -887,6 +1056,12 @@ type DomainEvent struct {
8871056
 	CreatedAt   pgtype.Timestamptz
8881057
 }
8891058
 
1059
+type DomainEventsProcessed struct {
1060
+	Consumer    string
1061
+	LastEventID int64
1062
+	UpdatedAt   pgtype.Timestamptz
1063
+}
1064
+
8901065
 type EmailVerification struct {
8911066
 	ID          int64
8921067
 	UserEmailID int64
@@ -1013,6 +1188,84 @@ type Milestone struct {
10131188
 	ClosedAt    pgtype.Timestamptz
10141189
 }
10151190
 
1191
+type Notification struct {
1192
+	ID              int64
1193
+	RecipientUserID int64
1194
+	Kind            string
1195
+	Reason          string
1196
+	RepoID          pgtype.Int8
1197
+	ThreadKind      NullNotificationThreadKind
1198
+	ThreadID        pgtype.Int8
1199
+	SourceEventID   pgtype.Int8
1200
+	Unread          bool
1201
+	LastEventAt     pgtype.Timestamptz
1202
+	LastActorUserID pgtype.Int8
1203
+	Summary         []byte
1204
+	CreatedAt       pgtype.Timestamptz
1205
+	UpdatedAt       pgtype.Timestamptz
1206
+}
1207
+
1208
+type NotificationEmailLog struct {
1209
+	ID              int64
1210
+	RecipientUserID int64
1211
+	NotificationID  pgtype.Int8
1212
+	ThreadKind      NullNotificationThreadKind
1213
+	ThreadID        pgtype.Int8
1214
+	SentAt          pgtype.Timestamptz
1215
+	MessageID       pgtype.Text
1216
+}
1217
+
1218
+type NotificationThread struct {
1219
+	RecipientUserID int64
1220
+	ThreadKind      NotificationThreadKind
1221
+	ThreadID        int64
1222
+	Subscribed      bool
1223
+	Reason          string
1224
+	UpdatedAt       pgtype.Timestamptz
1225
+}
1226
+
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
10161269
 type PasswordReset struct {
10171270
 	ID        int64
10181271
 	UserID    int64
@@ -1068,6 +1321,12 @@ type PrReviewRequest struct {
10681321
 	SatisfiedByReviewID pgtype.Int8
10691322
 }
10701323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
10711330
 type PullRequest struct {
10721331
 	IssueID            int64
10731332
 	BaseRef            string
internal/repos/sqlc/models.gomodified
@@ -362,6 +362,133 @@ func (ns NullMilestoneState) Value() (driver.Value, error) {
362362
 	return string(ns.MilestoneState), nil
363363
 }
364364
 
365
+type NotificationThreadKind string
366
+
367
+const (
368
+	NotificationThreadKindIssue NotificationThreadKind = "issue"
369
+	NotificationThreadKindPr    NotificationThreadKind = "pr"
370
+)
371
+
372
+func (e *NotificationThreadKind) Scan(src interface{}) error {
373
+	switch s := src.(type) {
374
+	case []byte:
375
+		*e = NotificationThreadKind(s)
376
+	case string:
377
+		*e = NotificationThreadKind(s)
378
+	default:
379
+		return fmt.Errorf("unsupported scan type for NotificationThreadKind: %T", src)
380
+	}
381
+	return nil
382
+}
383
+
384
+type NullNotificationThreadKind struct {
385
+	NotificationThreadKind NotificationThreadKind
386
+	Valid                  bool // Valid is true if NotificationThreadKind is not NULL
387
+}
388
+
389
+// Scan implements the Scanner interface.
390
+func (ns *NullNotificationThreadKind) Scan(value interface{}) error {
391
+	if value == nil {
392
+		ns.NotificationThreadKind, ns.Valid = "", false
393
+		return nil
394
+	}
395
+	ns.Valid = true
396
+	return ns.NotificationThreadKind.Scan(value)
397
+}
398
+
399
+// Value implements the driver Valuer interface.
400
+func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
401
+	if !ns.Valid {
402
+		return nil, nil
403
+	}
404
+	return string(ns.NotificationThreadKind), nil
405
+}
406
+
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
365492
 type PrFileStatus string
366493
 
367494
 const (
@@ -580,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
580707
 	return string(ns.PrReviewState), nil
581708
 }
582709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
583752
 type RepoInitStatus string
584753
 
585754
 const (
@@ -887,6 +1056,12 @@ type DomainEvent struct {
8871056
 	CreatedAt   pgtype.Timestamptz
8881057
 }
8891058
 
1059
+type DomainEventsProcessed struct {
1060
+	Consumer    string
1061
+	LastEventID int64
1062
+	UpdatedAt   pgtype.Timestamptz
1063
+}
1064
+
8901065
 type EmailVerification struct {
8911066
 	ID          int64
8921067
 	UserEmailID int64
@@ -1013,6 +1188,84 @@ type Milestone struct {
10131188
 	ClosedAt    pgtype.Timestamptz
10141189
 }
10151190
 
1191
+type Notification struct {
1192
+	ID              int64
1193
+	RecipientUserID int64
1194
+	Kind            string
1195
+	Reason          string
1196
+	RepoID          pgtype.Int8
1197
+	ThreadKind      NullNotificationThreadKind
1198
+	ThreadID        pgtype.Int8
1199
+	SourceEventID   pgtype.Int8
1200
+	Unread          bool
1201
+	LastEventAt     pgtype.Timestamptz
1202
+	LastActorUserID pgtype.Int8
1203
+	Summary         []byte
1204
+	CreatedAt       pgtype.Timestamptz
1205
+	UpdatedAt       pgtype.Timestamptz
1206
+}
1207
+
1208
+type NotificationEmailLog struct {
1209
+	ID              int64
1210
+	RecipientUserID int64
1211
+	NotificationID  pgtype.Int8
1212
+	ThreadKind      NullNotificationThreadKind
1213
+	ThreadID        pgtype.Int8
1214
+	SentAt          pgtype.Timestamptz
1215
+	MessageID       pgtype.Text
1216
+}
1217
+
1218
+type NotificationThread struct {
1219
+	RecipientUserID int64
1220
+	ThreadKind      NotificationThreadKind
1221
+	ThreadID        int64
1222
+	Subscribed      bool
1223
+	Reason          string
1224
+	UpdatedAt       pgtype.Timestamptz
1225
+}
1226
+
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
10161269
 type PasswordReset struct {
10171270
 	ID        int64
10181271
 	UserID    int64
@@ -1068,6 +1321,12 @@ type PrReviewRequest struct {
10681321
 	SatisfiedByReviewID pgtype.Int8
10691322
 }
10701323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
10711330
 type PullRequest struct {
10721331
 	IssueID            int64
10731332
 	BaseRef            string
internal/social/sqlc/models.gomodified
@@ -362,6 +362,133 @@ func (ns NullMilestoneState) Value() (driver.Value, error) {
362362
 	return string(ns.MilestoneState), nil
363363
 }
364364
 
365
+type NotificationThreadKind string
366
+
367
+const (
368
+	NotificationThreadKindIssue NotificationThreadKind = "issue"
369
+	NotificationThreadKindPr    NotificationThreadKind = "pr"
370
+)
371
+
372
+func (e *NotificationThreadKind) Scan(src interface{}) error {
373
+	switch s := src.(type) {
374
+	case []byte:
375
+		*e = NotificationThreadKind(s)
376
+	case string:
377
+		*e = NotificationThreadKind(s)
378
+	default:
379
+		return fmt.Errorf("unsupported scan type for NotificationThreadKind: %T", src)
380
+	}
381
+	return nil
382
+}
383
+
384
+type NullNotificationThreadKind struct {
385
+	NotificationThreadKind NotificationThreadKind
386
+	Valid                  bool // Valid is true if NotificationThreadKind is not NULL
387
+}
388
+
389
+// Scan implements the Scanner interface.
390
+func (ns *NullNotificationThreadKind) Scan(value interface{}) error {
391
+	if value == nil {
392
+		ns.NotificationThreadKind, ns.Valid = "", false
393
+		return nil
394
+	}
395
+	ns.Valid = true
396
+	return ns.NotificationThreadKind.Scan(value)
397
+}
398
+
399
+// Value implements the driver Valuer interface.
400
+func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
401
+	if !ns.Valid {
402
+		return nil, nil
403
+	}
404
+	return string(ns.NotificationThreadKind), nil
405
+}
406
+
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
365492
 type PrFileStatus string
366493
 
367494
 const (
@@ -580,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
580707
 	return string(ns.PrReviewState), nil
581708
 }
582709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
583752
 type RepoInitStatus string
584753
 
585754
 const (
@@ -887,6 +1056,12 @@ type DomainEvent struct {
8871056
 	CreatedAt   pgtype.Timestamptz
8881057
 }
8891058
 
1059
+type DomainEventsProcessed struct {
1060
+	Consumer    string
1061
+	LastEventID int64
1062
+	UpdatedAt   pgtype.Timestamptz
1063
+}
1064
+
8901065
 type EmailVerification struct {
8911066
 	ID          int64
8921067
 	UserEmailID int64
@@ -1013,6 +1188,84 @@ type Milestone struct {
10131188
 	ClosedAt    pgtype.Timestamptz
10141189
 }
10151190
 
1191
+type Notification struct {
1192
+	ID              int64
1193
+	RecipientUserID int64
1194
+	Kind            string
1195
+	Reason          string
1196
+	RepoID          pgtype.Int8
1197
+	ThreadKind      NullNotificationThreadKind
1198
+	ThreadID        pgtype.Int8
1199
+	SourceEventID   pgtype.Int8
1200
+	Unread          bool
1201
+	LastEventAt     pgtype.Timestamptz
1202
+	LastActorUserID pgtype.Int8
1203
+	Summary         []byte
1204
+	CreatedAt       pgtype.Timestamptz
1205
+	UpdatedAt       pgtype.Timestamptz
1206
+}
1207
+
1208
+type NotificationEmailLog struct {
1209
+	ID              int64
1210
+	RecipientUserID int64
1211
+	NotificationID  pgtype.Int8
1212
+	ThreadKind      NullNotificationThreadKind
1213
+	ThreadID        pgtype.Int8
1214
+	SentAt          pgtype.Timestamptz
1215
+	MessageID       pgtype.Text
1216
+}
1217
+
1218
+type NotificationThread struct {
1219
+	RecipientUserID int64
1220
+	ThreadKind      NotificationThreadKind
1221
+	ThreadID        int64
1222
+	Subscribed      bool
1223
+	Reason          string
1224
+	UpdatedAt       pgtype.Timestamptz
1225
+}
1226
+
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
10161269
 type PasswordReset struct {
10171270
 	ID        int64
10181271
 	UserID    int64
@@ -1068,6 +1321,12 @@ type PrReviewRequest struct {
10681321
 	SatisfiedByReviewID pgtype.Int8
10691322
 }
10701323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
10711330
 type PullRequest struct {
10721331
 	IssueID            int64
10731332
 	BaseRef            string
internal/users/sqlc/models.gomodified
@@ -362,6 +362,133 @@ func (ns NullMilestoneState) Value() (driver.Value, error) {
362362
 	return string(ns.MilestoneState), nil
363363
 }
364364
 
365
+type NotificationThreadKind string
366
+
367
+const (
368
+	NotificationThreadKindIssue NotificationThreadKind = "issue"
369
+	NotificationThreadKindPr    NotificationThreadKind = "pr"
370
+)
371
+
372
+func (e *NotificationThreadKind) Scan(src interface{}) error {
373
+	switch s := src.(type) {
374
+	case []byte:
375
+		*e = NotificationThreadKind(s)
376
+	case string:
377
+		*e = NotificationThreadKind(s)
378
+	default:
379
+		return fmt.Errorf("unsupported scan type for NotificationThreadKind: %T", src)
380
+	}
381
+	return nil
382
+}
383
+
384
+type NullNotificationThreadKind struct {
385
+	NotificationThreadKind NotificationThreadKind
386
+	Valid                  bool // Valid is true if NotificationThreadKind is not NULL
387
+}
388
+
389
+// Scan implements the Scanner interface.
390
+func (ns *NullNotificationThreadKind) Scan(value interface{}) error {
391
+	if value == nil {
392
+		ns.NotificationThreadKind, ns.Valid = "", false
393
+		return nil
394
+	}
395
+	ns.Valid = true
396
+	return ns.NotificationThreadKind.Scan(value)
397
+}
398
+
399
+// Value implements the driver Valuer interface.
400
+func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
401
+	if !ns.Valid {
402
+		return nil, nil
403
+	}
404
+	return string(ns.NotificationThreadKind), nil
405
+}
406
+
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
365492
 type PrFileStatus string
366493
 
367494
 const (
@@ -580,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
580707
 	return string(ns.PrReviewState), nil
581708
 }
582709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
583752
 type RepoInitStatus string
584753
 
585754
 const (
@@ -887,6 +1056,12 @@ type DomainEvent struct {
8871056
 	CreatedAt   pgtype.Timestamptz
8881057
 }
8891058
 
1059
+type DomainEventsProcessed struct {
1060
+	Consumer    string
1061
+	LastEventID int64
1062
+	UpdatedAt   pgtype.Timestamptz
1063
+}
1064
+
8901065
 type EmailVerification struct {
8911066
 	ID          int64
8921067
 	UserEmailID int64
@@ -1013,6 +1188,84 @@ type Milestone struct {
10131188
 	ClosedAt    pgtype.Timestamptz
10141189
 }
10151190
 
1191
+type Notification struct {
1192
+	ID              int64
1193
+	RecipientUserID int64
1194
+	Kind            string
1195
+	Reason          string
1196
+	RepoID          pgtype.Int8
1197
+	ThreadKind      NullNotificationThreadKind
1198
+	ThreadID        pgtype.Int8
1199
+	SourceEventID   pgtype.Int8
1200
+	Unread          bool
1201
+	LastEventAt     pgtype.Timestamptz
1202
+	LastActorUserID pgtype.Int8
1203
+	Summary         []byte
1204
+	CreatedAt       pgtype.Timestamptz
1205
+	UpdatedAt       pgtype.Timestamptz
1206
+}
1207
+
1208
+type NotificationEmailLog struct {
1209
+	ID              int64
1210
+	RecipientUserID int64
1211
+	NotificationID  pgtype.Int8
1212
+	ThreadKind      NullNotificationThreadKind
1213
+	ThreadID        pgtype.Int8
1214
+	SentAt          pgtype.Timestamptz
1215
+	MessageID       pgtype.Text
1216
+}
1217
+
1218
+type NotificationThread struct {
1219
+	RecipientUserID int64
1220
+	ThreadKind      NotificationThreadKind
1221
+	ThreadID        int64
1222
+	Subscribed      bool
1223
+	Reason          string
1224
+	UpdatedAt       pgtype.Timestamptz
1225
+}
1226
+
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
10161269
 type PasswordReset struct {
10171270
 	ID        int64
10181271
 	UserID    int64
@@ -1068,6 +1321,12 @@ type PrReviewRequest struct {
10681321
 	SatisfiedByReviewID pgtype.Int8
10691322
 }
10701323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
10711330
 type PullRequest struct {
10721331
 	IssueID            int64
10731332
 	BaseRef            string
internal/worker/sqlc/models.gomodified
@@ -362,6 +362,133 @@ func (ns NullMilestoneState) Value() (driver.Value, error) {
362362
 	return string(ns.MilestoneState), nil
363363
 }
364364
 
365
+type NotificationThreadKind string
366
+
367
+const (
368
+	NotificationThreadKindIssue NotificationThreadKind = "issue"
369
+	NotificationThreadKindPr    NotificationThreadKind = "pr"
370
+)
371
+
372
+func (e *NotificationThreadKind) Scan(src interface{}) error {
373
+	switch s := src.(type) {
374
+	case []byte:
375
+		*e = NotificationThreadKind(s)
376
+	case string:
377
+		*e = NotificationThreadKind(s)
378
+	default:
379
+		return fmt.Errorf("unsupported scan type for NotificationThreadKind: %T", src)
380
+	}
381
+	return nil
382
+}
383
+
384
+type NullNotificationThreadKind struct {
385
+	NotificationThreadKind NotificationThreadKind
386
+	Valid                  bool // Valid is true if NotificationThreadKind is not NULL
387
+}
388
+
389
+// Scan implements the Scanner interface.
390
+func (ns *NullNotificationThreadKind) Scan(value interface{}) error {
391
+	if value == nil {
392
+		ns.NotificationThreadKind, ns.Valid = "", false
393
+		return nil
394
+	}
395
+	ns.Valid = true
396
+	return ns.NotificationThreadKind.Scan(value)
397
+}
398
+
399
+// Value implements the driver Valuer interface.
400
+func (ns NullNotificationThreadKind) Value() (driver.Value, error) {
401
+	if !ns.Valid {
402
+		return nil, nil
403
+	}
404
+	return string(ns.NotificationThreadKind), nil
405
+}
406
+
407
+type OrgPlan string
408
+
409
+const (
410
+	OrgPlanFree       OrgPlan = "free"
411
+	OrgPlanTeam       OrgPlan = "team"
412
+	OrgPlanEnterprise OrgPlan = "enterprise"
413
+)
414
+
415
+func (e *OrgPlan) Scan(src interface{}) error {
416
+	switch s := src.(type) {
417
+	case []byte:
418
+		*e = OrgPlan(s)
419
+	case string:
420
+		*e = OrgPlan(s)
421
+	default:
422
+		return fmt.Errorf("unsupported scan type for OrgPlan: %T", src)
423
+	}
424
+	return nil
425
+}
426
+
427
+type NullOrgPlan struct {
428
+	OrgPlan OrgPlan
429
+	Valid   bool // Valid is true if OrgPlan is not NULL
430
+}
431
+
432
+// Scan implements the Scanner interface.
433
+func (ns *NullOrgPlan) Scan(value interface{}) error {
434
+	if value == nil {
435
+		ns.OrgPlan, ns.Valid = "", false
436
+		return nil
437
+	}
438
+	ns.Valid = true
439
+	return ns.OrgPlan.Scan(value)
440
+}
441
+
442
+// Value implements the driver Valuer interface.
443
+func (ns NullOrgPlan) Value() (driver.Value, error) {
444
+	if !ns.Valid {
445
+		return nil, nil
446
+	}
447
+	return string(ns.OrgPlan), nil
448
+}
449
+
450
+type OrgRole string
451
+
452
+const (
453
+	OrgRoleOwner  OrgRole = "owner"
454
+	OrgRoleMember OrgRole = "member"
455
+)
456
+
457
+func (e *OrgRole) Scan(src interface{}) error {
458
+	switch s := src.(type) {
459
+	case []byte:
460
+		*e = OrgRole(s)
461
+	case string:
462
+		*e = OrgRole(s)
463
+	default:
464
+		return fmt.Errorf("unsupported scan type for OrgRole: %T", src)
465
+	}
466
+	return nil
467
+}
468
+
469
+type NullOrgRole struct {
470
+	OrgRole OrgRole
471
+	Valid   bool // Valid is true if OrgRole is not NULL
472
+}
473
+
474
+// Scan implements the Scanner interface.
475
+func (ns *NullOrgRole) Scan(value interface{}) error {
476
+	if value == nil {
477
+		ns.OrgRole, ns.Valid = "", false
478
+		return nil
479
+	}
480
+	ns.Valid = true
481
+	return ns.OrgRole.Scan(value)
482
+}
483
+
484
+// Value implements the driver Valuer interface.
485
+func (ns NullOrgRole) Value() (driver.Value, error) {
486
+	if !ns.Valid {
487
+		return nil, nil
488
+	}
489
+	return string(ns.OrgRole), nil
490
+}
491
+
365492
 type PrFileStatus string
366493
 
367494
 const (
@@ -580,6 +707,48 @@ func (ns NullPrReviewState) Value() (driver.Value, error) {
580707
 	return string(ns.PrReviewState), nil
581708
 }
582709
 
710
+type PrincipalKind string
711
+
712
+const (
713
+	PrincipalKindUser PrincipalKind = "user"
714
+	PrincipalKindOrg  PrincipalKind = "org"
715
+)
716
+
717
+func (e *PrincipalKind) Scan(src interface{}) error {
718
+	switch s := src.(type) {
719
+	case []byte:
720
+		*e = PrincipalKind(s)
721
+	case string:
722
+		*e = PrincipalKind(s)
723
+	default:
724
+		return fmt.Errorf("unsupported scan type for PrincipalKind: %T", src)
725
+	}
726
+	return nil
727
+}
728
+
729
+type NullPrincipalKind struct {
730
+	PrincipalKind PrincipalKind
731
+	Valid         bool // Valid is true if PrincipalKind is not NULL
732
+}
733
+
734
+// Scan implements the Scanner interface.
735
+func (ns *NullPrincipalKind) Scan(value interface{}) error {
736
+	if value == nil {
737
+		ns.PrincipalKind, ns.Valid = "", false
738
+		return nil
739
+	}
740
+	ns.Valid = true
741
+	return ns.PrincipalKind.Scan(value)
742
+}
743
+
744
+// Value implements the driver Valuer interface.
745
+func (ns NullPrincipalKind) Value() (driver.Value, error) {
746
+	if !ns.Valid {
747
+		return nil, nil
748
+	}
749
+	return string(ns.PrincipalKind), nil
750
+}
751
+
583752
 type RepoInitStatus string
584753
 
585754
 const (
@@ -887,6 +1056,12 @@ type DomainEvent struct {
8871056
 	CreatedAt   pgtype.Timestamptz
8881057
 }
8891058
 
1059
+type DomainEventsProcessed struct {
1060
+	Consumer    string
1061
+	LastEventID int64
1062
+	UpdatedAt   pgtype.Timestamptz
1063
+}
1064
+
8901065
 type EmailVerification struct {
8911066
 	ID          int64
8921067
 	UserEmailID int64
@@ -1013,6 +1188,84 @@ type Milestone struct {
10131188
 	ClosedAt    pgtype.Timestamptz
10141189
 }
10151190
 
1191
+type Notification struct {
1192
+	ID              int64
1193
+	RecipientUserID int64
1194
+	Kind            string
1195
+	Reason          string
1196
+	RepoID          pgtype.Int8
1197
+	ThreadKind      NullNotificationThreadKind
1198
+	ThreadID        pgtype.Int8
1199
+	SourceEventID   pgtype.Int8
1200
+	Unread          bool
1201
+	LastEventAt     pgtype.Timestamptz
1202
+	LastActorUserID pgtype.Int8
1203
+	Summary         []byte
1204
+	CreatedAt       pgtype.Timestamptz
1205
+	UpdatedAt       pgtype.Timestamptz
1206
+}
1207
+
1208
+type NotificationEmailLog struct {
1209
+	ID              int64
1210
+	RecipientUserID int64
1211
+	NotificationID  pgtype.Int8
1212
+	ThreadKind      NullNotificationThreadKind
1213
+	ThreadID        pgtype.Int8
1214
+	SentAt          pgtype.Timestamptz
1215
+	MessageID       pgtype.Text
1216
+}
1217
+
1218
+type NotificationThread struct {
1219
+	RecipientUserID int64
1220
+	ThreadKind      NotificationThreadKind
1221
+	ThreadID        int64
1222
+	Subscribed      bool
1223
+	Reason          string
1224
+	UpdatedAt       pgtype.Timestamptz
1225
+}
1226
+
1227
+type Org struct {
1228
+	ID                    int64
1229
+	Slug                  string
1230
+	DisplayName           string
1231
+	Description           string
1232
+	AvatarObjectKey       pgtype.Text
1233
+	Location              string
1234
+	Website               string
1235
+	BillingEmail          string
1236
+	Plan                  OrgPlan
1237
+	AllowMemberRepoCreate bool
1238
+	CreatedByUserID       pgtype.Int8
1239
+	SuspendedAt           pgtype.Timestamptz
1240
+	SuspendedReason       pgtype.Text
1241
+	DeletedAt             pgtype.Timestamptz
1242
+	CreatedAt             pgtype.Timestamptz
1243
+	UpdatedAt             pgtype.Timestamptz
1244
+}
1245
+
1246
+type OrgInvitation struct {
1247
+	ID              int64
1248
+	OrgID           int64
1249
+	InvitedByUserID pgtype.Int8
1250
+	TargetUserID    pgtype.Int8
1251
+	TargetEmail     pgtype.Text
1252
+	Role            OrgRole
1253
+	TokenHash       []byte
1254
+	ExpiresAt       pgtype.Timestamptz
1255
+	AcceptedAt      pgtype.Timestamptz
1256
+	DeclinedAt      pgtype.Timestamptz
1257
+	CanceledAt      pgtype.Timestamptz
1258
+	CreatedAt       pgtype.Timestamptz
1259
+}
1260
+
1261
+type OrgMember struct {
1262
+	OrgID           int64
1263
+	UserID          int64
1264
+	Role            OrgRole
1265
+	InvitedByUserID pgtype.Int8
1266
+	JoinedAt        pgtype.Timestamptz
1267
+}
1268
+
10161269
 type PasswordReset struct {
10171270
 	ID        int64
10181271
 	UserID    int64
@@ -1068,6 +1321,12 @@ type PrReviewRequest struct {
10681321
 	SatisfiedByReviewID pgtype.Int8
10691322
 }
10701323
 
1324
+type Principal struct {
1325
+	Slug string
1326
+	Kind PrincipalKind
1327
+	ID   int64
1328
+}
1329
+
10711330
 type PullRequest struct {
10721331
 	IssueID            int64
10731332
 	BaseRef            string
sqlc.yamlmodified
@@ -145,3 +145,35 @@ sql:
145145
         emit_exact_table_names: false
146146
         emit_empty_slices: true
147147
         emit_methods_with_db_argument: true
148
+
149
+  - engine: postgresql
150
+    schema: internal/migrationsfs/migrations
151
+    queries: internal/notif/queries
152
+    gen:
153
+      go:
154
+        package: notifdb
155
+        out: internal/notif/sqlc
156
+        sql_package: pgx/v5
157
+        emit_json_tags: false
158
+        emit_pointers_for_null_types: false
159
+        emit_prepared_queries: false
160
+        emit_interface: true
161
+        emit_exact_table_names: false
162
+        emit_empty_slices: true
163
+        emit_methods_with_db_argument: true
164
+
165
+  - engine: postgresql
166
+    schema: internal/migrationsfs/migrations
167
+    queries: internal/orgs/queries
168
+    gen:
169
+      go:
170
+        package: orgsdb
171
+        out: internal/orgs/sqlc
172
+        sql_package: pgx/v5
173
+        emit_json_tags: false
174
+        emit_pointers_for_null_types: false
175
+        emit_prepared_queries: false
176
+        emit_interface: true
177
+        emit_exact_table_names: false
178
+        emit_empty_slices: true
179
+        emit_methods_with_db_argument: true