Facets enable components to extend Person/Group with typed preferences and settings. Each component defines facet schemas (e.g., TodoPreferencesFacet) while Identity handles storage. Facets are namespaced by component (todos.preferences, finance.settings) and support schema versioning for forward-compatible migrations. Changes emit events for sync.

1
Ports
2
Schemas
0
Hooks
2
Events
01

Ports

Required
  • Clock
    Track created_at and updated_at timestamps
Optional
  • EventBus optional
    Publish FacetUpserted/FacetDeleted events
Adapters Provided
  • InMemoryFacetStore
    implements FacetReaderPort
    In-memory facet storage for testing
02

Schemas

Defines
Uses

No external schemas used

03

Hooks

No hooks declared

04

Events

  • FacetUpserted v1
    After a facet is created or updated
    payload: {owner_type, owner_id, facet_name, schema_version}
  • FacetDeleted v1
    After a facet is deleted
    payload: {owner_type, owner_id, facet_name}
05

Stories

06

Examples

Define a typed facet
from dataclasses import dataclass, field, asdict
from typing import Any

from psp.platform.facets import FacetPayload


@dataclass
class TodoPreferencesFacet(FacetPayload):
    """Per-person todo preferences."""

    schema_version: int = 1
    default_priority: str = "medium"
    default_container_id: str | None = None
    show_completed_tasks: bool = False
    auto_archive_after_days: int | None = None

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)

Create a dataclass implementing FacetPayload.

Read facet in a use case
from psp.platform.facets import FacetKey, FacetReaderPort


class CreateTask:
    def __init__(self, ..., facet_reader: FacetReaderPort):
        self._facet_reader = facet_reader

    def execute(self, input: CreateTaskInput, actor_id: UUID) -> Task:
        # Load user preferences
        key = FacetKey("person", actor_id, "todos.preferences")
        prefs = self._facet_reader.get(key, TodoPreferencesFacet)

        # Apply defaults from preferences
        priority = input.priority
        if priority is None and prefs:
            priority = prefs.default_priority
        priority = priority or "medium"

        return Task(title=input.title, priority=priority, ...)

Inject FacetReaderPort and load user preferences.

Write facet in a use case
from psp.platform.facets import FacetKey, FacetWriterPort


class UpdateTodoPreferences:
    def __init__(self, facet_writer: FacetWriterPort):
        self._facet_writer = facet_writer

    def execute(self, actor_id: UUID, input: UpdatePreferencesInput) -> None:
        key = FacetKey("person", actor_id, "todos.preferences")

        prefs = TodoPreferencesFacet(
            default_priority=input.default_priority,
            show_completed_tasks=input.show_completed,
        )

        self._facet_writer.upsert(key, prefs)

Inject FacetWriterPort to update user preferences.

Register facet schema in module
class TodosModule(Module):
    FACETS = {
        "todos.preferences": TodoPreferencesFacet,
        "todos.settings": TodoSettingsFacet,
    }

    def register(self, registry: ComponentRegistry, ctx: RuntimeContext) -> None:
        # Register facet schemas so Identity knows how to validate
        for facet_key, facet_type in self.FACETS.items():
            registry.add_facet_schema(facet_key, facet_type)

Components register facet schemas during startup.

API Reference

This component mounts routes under /v1/facets. View OpenAPI specification