Files
member-console/openspec/specs/identity/spec.md

4.1 KiB

identity

Purpose

Defines the identity model: users (authentication records) and persons (human identity records) with UUIDv7 primary keys.

Requirements

Requirement: Split identity model

The system SHALL maintain two distinct identity tables: users for authentication identity and persons for business identity. A users record represents how an actor authenticates (OIDC subject, login state). A persons record represents who an actor is in the business domain (name, email, status). The two are linked by persons.user_id → users.user_id.

Scenario: New user authenticates via OIDC

  • WHEN a user completes OIDC authentication and no users record exists for their OIDC subject
  • THEN the system SHALL create a users record with the OIDC subject and an active status
  • AND the system SHALL create a persons record linked to that user, populated with display name and email from the OIDC claims

Scenario: Returning user authenticates

  • WHEN a user completes OIDC authentication and a users record already exists for their OIDC subject
  • THEN the system SHALL update last_login_at and last_login_ip on the users record
  • AND the system SHALL update the persons record if the display name or email from OIDC claims has changed

Requirement: Users table schema

The users table SHALL contain the following fields: user_id (UUIDv7 primary key, DEFAULT uuidv7()), oidc_subject (TEXT, UNIQUE, NOT NULL), status (VARCHAR(20), NOT NULL, DEFAULT 'active'), last_login_at (TIMESTAMPTZ), last_login_ip (INET), created_at (TIMESTAMPTZ, NOT NULL, DEFAULT NOW()), updated_at (TIMESTAMPTZ, NOT NULL, DEFAULT NOW()).

Scenario: User ID is a UUIDv7

  • WHEN a new users record is inserted without an explicit user_id
  • THEN the database SHALL generate a UUIDv7 via the uuidv7() function

Scenario: OIDC subject uniqueness

  • WHEN an attempt is made to insert a users record with an oidc_subject that already exists
  • THEN the database SHALL reject the insert with a unique constraint violation

Requirement: Persons table schema

The persons table SHALL contain the following fields: person_id (UUIDv7 primary key, DEFAULT uuidv7()), user_id (UUID, FK → users, UNIQUE), display_name (VARCHAR(255), NOT NULL), primary_email (VARCHAR(255), NOT NULL), primary_email_verified (BOOLEAN, NOT NULL, DEFAULT FALSE), status (VARCHAR(20), NOT NULL, DEFAULT 'active'), created_at (TIMESTAMPTZ, NOT NULL, DEFAULT NOW()), updated_at (TIMESTAMPTZ, NOT NULL, DEFAULT NOW()).

Scenario: Person linked to user

  • WHEN a persons record is created
  • THEN it SHALL reference exactly one users record via user_id
  • AND the user_id value SHALL be unique across all persons records (1:1 relationship)

Scenario: Person survives user deactivation

  • WHEN a users record has its status set to deactivated
  • THEN the corresponding persons record SHALL remain with status = 'active' (business identity persists independently of auth state)

Requirement: Identity module owns its schema

The identity module SHALL own its database schema via migrations in internal/identity/migrations/. The identity module SHALL own its queries via sqlc configuration in internal/identity/sqlc.yaml generating into the internal/identity/ Go package.

Scenario: Identity migration creates tables

  • WHEN the identity module's migration runs against an empty database
  • THEN the users and persons tables SHALL exist with all specified columns, constraints, and indexes

Scenario: Identity sqlc generates typed queries

  • WHEN sqlc generate is run from the identity module
  • THEN it SHALL produce Go types and query methods for users and persons in the identity package

Requirement: Updated at trigger

The system SHALL automatically update the updated_at timestamp on users and persons rows whenever they are modified.

Scenario: User record updated

  • WHEN any field on a users record is modified
  • THEN the updated_at field SHALL be set to the current timestamp via a database trigger