[{"data":1,"prerenderedAt":7034},["ShallowReactive",2],{"navigation":3,"\u002Freference\u002Fdata-models":189,"\u002Freference\u002Fdata-models-surround":7029},[4,34,60,116,137,153],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":33},"Getting Started","i-lucide-rocket","\u002Fgetting-started","1.getting-started",[10,15,18,23,28],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fgetting-started\u002Fintroduction","1.getting-started\u002F1.introduction","i-lucide-house",{"title":5,"path":16,"stem":17,"icon":6},"\u002Fgetting-started\u002Fgetting-started","1.getting-started\u002F2.getting-started",{"title":19,"path":20,"stem":21,"icon":22},"Choosing a Preset","\u002Fgetting-started\u002Fchoosing-a-preset","1.getting-started\u002F3.choosing-a-preset","i-lucide-git-branch",{"title":24,"path":25,"stem":26,"icon":27},"Deployment","\u002Fgetting-started\u002Fdeployment","1.getting-started\u002F4.deployment","i-lucide-cloud",{"title":29,"path":30,"stem":31,"icon":32},"Background Maintenance","\u002Fgetting-started\u002Fbackground-maintenance","1.getting-started\u002F5.background-maintenance","i-lucide-timer-reset",false,{"title":35,"icon":36,"path":37,"stem":38,"children":39,"page":33},"Build","i-lucide-wrench","\u002Fbuild","2.build",[40,45,50,55],{"title":41,"path":42,"stem":43,"icon":44},"Routers & Prefixes","\u002Fbuild\u002Frouters-and-prefixes","2.build\u002F1.routers-and-prefixes","i-lucide-route",{"title":46,"path":47,"stem":48,"icon":49},"Configuration","\u002Fbuild\u002Fconfiguration","2.build\u002F2.configuration","i-lucide-settings",{"title":51,"path":52,"stem":53,"icon":54},"Authorization Dependencies","\u002Fbuild\u002Fauthorization-dependencies","2.build\u002F3.authorization-dependencies","i-lucide-shield-check",{"title":56,"path":57,"stem":58,"icon":59},"Command Line","\u002Fbuild\u002Fcli","2.build\u002F4.cli","i-lucide-terminal",{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":33},"Auth","i-lucide-lock","\u002Fauth","3.auth",[66,71,76,81,86,91,96,101,106,111],{"title":67,"path":68,"stem":69,"icon":70},"OAuth & Social Login","\u002Fauth\u002Foauth-and-social-login","3.auth\u002F1.oauth-and-social-login","i-lucide-log-in",{"title":72,"path":73,"stem":74,"icon":75},"Multi-Frontend Support","\u002Fauth\u002Fmulti-frontend","3.auth\u002F10.multi-frontend","i-lucide-layout-grid",{"title":77,"path":78,"stem":79,"icon":80},"Sessions & Audit","\u002Fauth\u002Fsessions-and-audit","3.auth\u002F2.sessions-and-audit","i-lucide-monitor-smartphone",{"title":82,"path":83,"stem":84,"icon":85},"Passwordless & Messaging","\u002Fauth\u002Fpasswordless-and-messaging","3.auth\u002F3.passwordless-and-messaging","i-lucide-mail",{"title":87,"path":88,"stem":89,"icon":90},"JWT Tokens","\u002Fauth\u002Fjwt-tokens","3.auth\u002F4.jwt-tokens","i-lucide-key-round",{"title":92,"path":93,"stem":94,"icon":95},"User Management API","\u002Fauth\u002Fuser-management-api","3.auth\u002F5.user-management-api","i-lucide-users",{"title":97,"path":98,"stem":99,"icon":100},"User Invitations","\u002Fauth\u002Fuser-invitations","3.auth\u002F6.user-invitations","i-lucide-send",{"title":102,"path":103,"stem":104,"icon":105},"Roles & Permissions","\u002Fauth\u002Froles-and-permissions","3.auth\u002F7.roles-and-permissions","i-lucide-shield",{"title":107,"path":108,"stem":109,"icon":110},"ABAC","\u002Fauth\u002Fabac","3.auth\u002F8.abac","i-lucide-filter",{"title":112,"path":113,"stem":114,"icon":115},"User Status","\u002Fauth\u002Fuser-status","3.auth\u002F9.user-status","i-lucide-user-cog",{"title":117,"icon":118,"path":119,"stem":120,"children":121,"page":33},"Enterprise","i-lucide-building-2","\u002Fenterprise","4.enterprise",[122,127,132],{"title":123,"path":124,"stem":125,"icon":126},"Core Authorization Concepts","\u002Fenterprise\u002Fcore-authorization-concepts","4.enterprise\u002F1.core-authorization-concepts","i-lucide-network",{"title":128,"path":129,"stem":130,"icon":131},"Entities","\u002Fenterprise\u002Fentities","4.enterprise\u002F2.entities","i-lucide-folder-tree",{"title":133,"path":134,"stem":135,"icon":136},"Entity Memberships","\u002Fenterprise\u002Fentity-memberships","4.enterprise\u002F3.entity-memberships","i-lucide-user-plus",{"title":138,"icon":139,"path":140,"stem":141,"children":142,"page":33},"Integrations","i-lucide-plug","\u002Fintegrations","5.integrations",[143,148],{"title":144,"path":145,"stem":146,"icon":147},"API Keys","\u002Fintegrations\u002Fapi-keys","5.integrations\u002F1.api-keys","i-lucide-key",{"title":149,"path":150,"stem":151,"icon":152},"OutlabsAuth UI","\u002Fintegrations\u002Foutlabsauth-ui","5.integrations\u002F2.outlabsauth-ui","i-lucide-layout-dashboard",{"title":154,"icon":155,"path":156,"stem":157,"children":158,"page":33},"Reference","i-lucide-book-marked","\u002Freference","6.reference",[159,164,169,174,179,184],{"title":160,"path":161,"stem":162,"icon":163},"Data Models","\u002Freference\u002Fdata-models","6.reference\u002F1.data-models","i-lucide-database",{"title":165,"path":166,"stem":167,"icon":168},"Activity Tracking","\u002Freference\u002Factivity-tracking","6.reference\u002F2.activity-tracking","i-lucide-activity",{"title":170,"path":171,"stem":172,"icon":173},"Testing","\u002Freference\u002Ftesting","6.reference\u002F3.testing","i-lucide-flask-conical",{"title":175,"path":176,"stem":177,"icon":178},"Observability","\u002Freference\u002Fobservability","6.reference\u002F4.observability","i-lucide-eye",{"title":180,"path":181,"stem":182,"icon":183},"Metrics Reference","\u002Freference\u002Fmetrics-reference","6.reference\u002F5.metrics-reference","i-lucide-chart-bar",{"title":185,"path":186,"stem":187,"icon":188},"Log Events Reference","\u002Freference\u002Flog-events-reference","6.reference\u002F6.log-events-reference","i-lucide-scroll-text",{"id":190,"title":160,"body":191,"description":7022,"extension":7023,"links":7024,"meta":7025,"navigation":7026,"path":161,"seo":7027,"stem":162,"__hash__":7028},"docs\u002F6.reference\u002F1.data-models.md",{"type":192,"value":193,"toc":6963},"minimark",[194,201,206,227,238,251,270,273,277,287,289,293,310,475,478,498,504,532,534,538,543,552,883,906,915,969,974,1125,1130,1155,1166,1221,1239,1252,1257,1363,1365,1369,1377,1383,1481,1616,1621,1656,1667,1671,1756,1760,1915,1930,1932,1936,1944,2032,2036,2066,2087,2099,2101,2105,2110,2119,2281,2295,2299,2334,2338,2395,2399,2447,2449,2453,2460,2467,2526,2711,2715,2745,2749,2877,2888,2892,3002,3008,3010,3014,3019,3160,3164,3184,3188,3245,3250,3252,3256,3260,3268,3501,3520,3526,3532,3609,3616,3620,3650,3654,3736,3746,3750,3854,3859,3861,3865,3870,3881,4016,4020,4050,4060,4064,4106,4110,4209,4214,4216,4220,4227,4270,4274,4294,4301,4305,4360,4365,4485,4490,4492,4496,4501,4608,4613,4633,4638,4678,4682,4749,4754,4768,4777,4779,4783,4792,4796,4814,4818,4821,4827,4831,4837,4843,4847,4889,4893,4896,5006,5010,5013,5052,5056,5059,5078,5085,5087,5091,5095,5100,5292,5296,5394,5402,5404,5408,5413,5437,5441,5525,5527,5531,5535,5540,5655,5664,5666,5670,5675,5719,5723,5743,5749,5754,5776,5783,5791,5793,5797,5812,5931,5933,5937,5942,5947,6071,6079,6137,6145,6166,6208,6210,6214,6218,6222,6237,6242,6251,6256,6271,6276,6290,6294,6309,6314,6333,6338,6357,6370,6372,6376,6380,6480,6484,6521,6523,6526,6530,6533,6538,6557,6562,6576,6580,6598,6608,6610,6614,6618,6660,6664,6720,6724,6761,6765,6768,6812,6814,6818,6880,6888,6890,6894,6924,6928,6959],[195,196,197],"note",{},[198,199,200],"p",{},"— schema lookup for implementers. Not a getting-started guide.",[202,203,205],"h2",{"id":204},"overview","Overview",[198,207,208,209,213,214,217,218,222,223,226],{},"OutlabsAuth uses ",[210,211,212],"strong",{},"PostgreSQL"," with ",[210,215,216],{},"SQLAlchemy\u002FSQLModel"," (async). Common\nfields (",[219,220,221],"code",{},"id",", timestamps) come from ",[219,224,225],{},"outlabs_auth.database.base.BaseModel",".",[198,228,229,230,233,234,237],{},"This page is a ",[210,231,232],{},"schema reference",", not a tutorial — start with\n",[235,236,5],"a",{"href":16}," if you are wiring an app.",[198,239,240,243,244,247,250],{},[210,241,242],{},"Database",": PostgreSQL 14+",[245,246],"br",{},[210,248,249],{},"ORM",": SQLAlchemy async + SQLModel (driver: asyncpg)",[198,252,253,254,257,258,261,262,265,266,269],{},"Not every table is created for every configuration -\n",[219,255,256],{},"outlabs_auth\u002Fdatabase\u002Fregistry.py"," selects the table set from the feature\nflags (for example ",[219,259,260],{},"entity_*"," tables only when ",[219,263,264],{},"enable_entity_hierarchy=True",",\notherwise ",[219,267,268],{},"user_role_memberships",").",[271,272],"hr",{},[202,274,276],{"id":275},"model-hierarchy","Model Hierarchy",[278,279,284],"pre",{"className":280,"code":282,"language":283},[281],"language-text","SQLModel\n└─ BaseModel  (id, created_at, updated_at — not a table itself)\n   ├─ User\n   ├─ Role\n   ├─ RolePermission (junction)\n   ├─ RoleCondition\n   ├─ RoleEntityTypePermission\n   ├─ RoleDefinitionHistory\n   ├─ Permission\n   ├─ PermissionTag\n   ├─ PermissionTagLink (junction)\n   ├─ PermissionCondition\n   ├─ PermissionDefinitionHistory\n   ├─ ConditionGroup\n   ├─ RefreshToken\n   ├─ AuthChallenge\n   ├─ APIKey\n   ├─ APIKeyScope (junction)\n   ├─ APIKeyIPWhitelist\n   ├─ APIKeyUsageSyncBatch\n   ├─ SocialAccount\n   ├─ OAuthState\n   ├─ IntegrationPrincipal\n   ├─ IntegrationPrincipalRole (junction)\n   ├─ UserRoleMembership (SimpleRBAC)\n   ├─ UserAuditEvent\n   ├─ ActivityMetric\n   ├─ UserActivity\n   ├─ LoginHistory\n   ├─ SystemConfig\n   ├─ Entity (EnterpriseRBAC)\n   ├─ EntityMembership (EnterpriseRBAC)\n   ├─ EntityMembershipRole (junction)\n   ├─ EntityMembershipHistory (EnterpriseRBAC)\n   └─ EntityClosure (EnterpriseRBAC)\n","text",[219,285,282],{"__ignoreMap":286},"",[271,288],{},[202,290,292],{"id":291},"base-model","Base Model",[198,294,295,298,299,302,303,306,307,226],{},[219,296,297],{},"BaseModel"," (",[219,300,301],{},"outlabs_auth\u002Fdatabase\u002Fbase.py",") is the common base for all\ntables. It is ",[210,304,305],{},"not"," a table itself - child classes must set ",[219,308,309],{},"table=True",[278,311,315],{"className":312,"code":313,"language":314,"meta":286,"style":286},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","from datetime import datetime, timezone\nfrom uuid import UUID, uuid4\n\nfrom sqlalchemy import func\nfrom sqlalchemy.dialects.postgresql import TIMESTAMP\nfrom sqlalchemy.dialects.postgresql import UUID as PG_UUID\nfrom sqlmodel import Field, SQLModel\n\n\nclass BaseModel(SQLModel):\n    id: UUID = Field(\n        default_factory=uuid4,\n        primary_key=True,\n        sa_type=PG_UUID(as_uuid=True),\n    )\n    created_at: datetime = Field(\n        default_factory=lambda: datetime.now(timezone.utc),\n        sa_type=TIMESTAMP(timezone=True),\n        nullable=False,\n        sa_column_kwargs={\"server_default\": func.now()},\n    )\n    updated_at: datetime = Field(\n        default_factory=lambda: datetime.now(timezone.utc),\n        sa_type=TIMESTAMP(timezone=True),\n        nullable=False,\n        sa_column_kwargs={\"server_default\": func.now(), \"onupdate\": func.now()},\n    )\n","python",[219,316,317,325,331,338,344,350,356,362,367,372,378,384,390,396,402,408,414,420,426,432,438,443,449,454,459,464,470],{"__ignoreMap":286},[318,319,322],"span",{"class":320,"line":321},"line",1,[318,323,324],{},"from datetime import datetime, timezone\n",[318,326,328],{"class":320,"line":327},2,[318,329,330],{},"from uuid import UUID, uuid4\n",[318,332,334],{"class":320,"line":333},3,[318,335,337],{"emptyLinePlaceholder":336},true,"\n",[318,339,341],{"class":320,"line":340},4,[318,342,343],{},"from sqlalchemy import func\n",[318,345,347],{"class":320,"line":346},5,[318,348,349],{},"from sqlalchemy.dialects.postgresql import TIMESTAMP\n",[318,351,353],{"class":320,"line":352},6,[318,354,355],{},"from sqlalchemy.dialects.postgresql import UUID as PG_UUID\n",[318,357,359],{"class":320,"line":358},7,[318,360,361],{},"from sqlmodel import Field, SQLModel\n",[318,363,365],{"class":320,"line":364},8,[318,366,337],{"emptyLinePlaceholder":336},[318,368,370],{"class":320,"line":369},9,[318,371,337],{"emptyLinePlaceholder":336},[318,373,375],{"class":320,"line":374},10,[318,376,377],{},"class BaseModel(SQLModel):\n",[318,379,381],{"class":320,"line":380},11,[318,382,383],{},"    id: UUID = Field(\n",[318,385,387],{"class":320,"line":386},12,[318,388,389],{},"        default_factory=uuid4,\n",[318,391,393],{"class":320,"line":392},13,[318,394,395],{},"        primary_key=True,\n",[318,397,399],{"class":320,"line":398},14,[318,400,401],{},"        sa_type=PG_UUID(as_uuid=True),\n",[318,403,405],{"class":320,"line":404},15,[318,406,407],{},"    )\n",[318,409,411],{"class":320,"line":410},16,[318,412,413],{},"    created_at: datetime = Field(\n",[318,415,417],{"class":320,"line":416},17,[318,418,419],{},"        default_factory=lambda: datetime.now(timezone.utc),\n",[318,421,423],{"class":320,"line":422},18,[318,424,425],{},"        sa_type=TIMESTAMP(timezone=True),\n",[318,427,429],{"class":320,"line":428},19,[318,430,431],{},"        nullable=False,\n",[318,433,435],{"class":320,"line":434},20,[318,436,437],{},"        sa_column_kwargs={\"server_default\": func.now()},\n",[318,439,441],{"class":320,"line":440},21,[318,442,407],{},[318,444,446],{"class":320,"line":445},22,[318,447,448],{},"    updated_at: datetime = Field(\n",[318,450,452],{"class":320,"line":451},23,[318,453,419],{},[318,455,457],{"class":320,"line":456},24,[318,458,425],{},[318,460,462],{"class":320,"line":461},25,[318,463,431],{},[318,465,467],{"class":320,"line":466},26,[318,468,469],{},"        sa_column_kwargs={\"server_default\": func.now(), \"onupdate\": func.now()},\n",[318,471,473],{"class":320,"line":472},27,[318,474,407],{},[198,476,477],{},"Table models then inherit it:",[278,479,481],{"className":312,"code":480,"language":314,"meta":286,"style":286},"class User(BaseModel, table=True):\n    __tablename__ = \"users\"\n    ...\n",[219,482,483,488,493],{"__ignoreMap":286},[318,484,485],{"class":320,"line":321},[318,486,487],{},"class User(BaseModel, table=True):\n",[318,489,490],{"class":320,"line":327},[318,491,492],{},"    __tablename__ = \"users\"\n",[318,494,495],{"class":320,"line":333},[318,496,497],{},"    ...\n",[198,499,500,503],{},[210,501,502],{},"Common Fields",":",[505,506,507,513,523],"ul",{},[508,509,510,512],"li",{},[219,511,221],{}," - UUID v4 primary key (auto-generated)",[508,514,515,518,519,522],{},[219,516,517],{},"created_at"," - Timestamp when record was created (UTC, ",[219,520,521],{},"server_default now()",")",[508,524,525,528,529,522],{},[219,526,527],{},"updated_at"," - Timestamp when record was last modified (UTC, ",[219,530,531],{},"onupdate now()",[271,533],{},[202,535,537],{"id":536},"core-models","Core Models",[539,540,542],"h3",{"id":541},"_1-user","1. User",[198,544,545,548,549],{},[210,546,547],{},"User accounts and authentication."," Table: ",[219,550,551],{},"users",[278,553,555],{"className":312,"code":554,"language":314,"meta":286,"style":286},"class UserStatus(str, Enum):\n    \"\"\"User account status controlling authentication access.\n\n    See DD-048 (docs-library\u002F48-User-Status-System.md) for detailed semantics.\n    \"\"\"\n    ACTIVE = \"active\"        # Can authenticate ✅\n    SUSPENDED = \"suspended\"  # Temporary block (with optional expiry) ❌\n    BANNED = \"banned\"        # Permanent block ❌\n    DELETED = \"deleted\"      # Soft-deleted (with deletion timestamp) ❌\n\nclass User(BaseModel, table=True):\n    __tablename__ = \"users\"\n\n    # Authentication\n    email: EmailStr  # Unique (uq_users_email)\n    hashed_password: Optional[str]  # None for OAuth-only users\n    auth_methods: List[str] = [\"PASSWORD\"]  # e.g., [\"PASSWORD\", \"GOOGLE\", \"GITHUB\"]\n\n    # Basic Identity (optional - see \"Extending User\" below for full profiles)\n    first_name: Optional[str] = None  # Optional display name\n    last_name: Optional[str] = None   # Optional display name\n\n    # Organization Binding (EnterpriseRBAC) - See DD-050\n    root_entity_id: Optional[UUID] = None  # Root entity this user belongs to\n    # NULL for superusers or unassigned users\n    # Set automatically on first membership assignment\n    # Users can only belong to ONE root entity (organization)\n\n    # Profile\n    avatar_url: Optional[str] = None\n    phone: Optional[str] = None\n    locale: Optional[str] = None        # e.g. \"en-US\"\n    timezone: Optional[str] = None      # e.g. \"America\u002FNew_York\"\n\n    # Status\n    status: UserStatus = UserStatus.ACTIVE\n    suspended_until: Optional[datetime] = None  # Auto-expiry for SUSPENDED status\n    deleted_at: Optional[datetime] = None       # Timestamp for DELETED status\n    is_superuser: bool = False\n    email_verified: bool = False\n    phone_verified: bool = False\n\n    # Security & Activity Tracking\n    last_login: Optional[datetime] = None          # Only on email\u002Fpassword or OAuth login\n    last_activity: Optional[datetime] = None       # Any authenticated action (DD-049)\n    last_password_change: Optional[datetime] = None\n    failed_login_attempts: int = 0\n    locked_until: Optional[datetime] = None\n\n    # Password Reset\n    password_reset_token: Optional[str] = None\n    password_reset_expires: Optional[datetime] = None\n\n    # Email Verification\n    email_verification_token: Optional[str] = None\n    email_verification_expires: Optional[datetime] = None\n\n    # Invitation\n    invite_token: Optional[str] = None          # SHA-256 hash of the invite token\n    invite_token_expires: Optional[datetime] = None\n    invited_by_id: Optional[UUID] = None        # FK -> users.id\n",[219,556,557,562,567,571,576,581,586,591,596,601,605,609,613,617,622,627,632,637,641,646,651,656,660,665,670,675,680,685,690,696,702,708,714,720,725,731,737,743,749,755,761,767,772,778,784,790,796,802,808,813,819,825,831,836,842,848,854,859,865,871,877],{"__ignoreMap":286},[318,558,559],{"class":320,"line":321},[318,560,561],{},"class UserStatus(str, Enum):\n",[318,563,564],{"class":320,"line":327},[318,565,566],{},"    \"\"\"User account status controlling authentication access.\n",[318,568,569],{"class":320,"line":333},[318,570,337],{"emptyLinePlaceholder":336},[318,572,573],{"class":320,"line":340},[318,574,575],{},"    See DD-048 (docs-library\u002F48-User-Status-System.md) for detailed semantics.\n",[318,577,578],{"class":320,"line":346},[318,579,580],{},"    \"\"\"\n",[318,582,583],{"class":320,"line":352},[318,584,585],{},"    ACTIVE = \"active\"        # Can authenticate ✅\n",[318,587,588],{"class":320,"line":358},[318,589,590],{},"    SUSPENDED = \"suspended\"  # Temporary block (with optional expiry) ❌\n",[318,592,593],{"class":320,"line":364},[318,594,595],{},"    BANNED = \"banned\"        # Permanent block ❌\n",[318,597,598],{"class":320,"line":369},[318,599,600],{},"    DELETED = \"deleted\"      # Soft-deleted (with deletion timestamp) ❌\n",[318,602,603],{"class":320,"line":374},[318,604,337],{"emptyLinePlaceholder":336},[318,606,607],{"class":320,"line":380},[318,608,487],{},[318,610,611],{"class":320,"line":386},[318,612,492],{},[318,614,615],{"class":320,"line":392},[318,616,337],{"emptyLinePlaceholder":336},[318,618,619],{"class":320,"line":398},[318,620,621],{},"    # Authentication\n",[318,623,624],{"class":320,"line":404},[318,625,626],{},"    email: EmailStr  # Unique (uq_users_email)\n",[318,628,629],{"class":320,"line":410},[318,630,631],{},"    hashed_password: Optional[str]  # None for OAuth-only users\n",[318,633,634],{"class":320,"line":416},[318,635,636],{},"    auth_methods: List[str] = [\"PASSWORD\"]  # e.g., [\"PASSWORD\", \"GOOGLE\", \"GITHUB\"]\n",[318,638,639],{"class":320,"line":422},[318,640,337],{"emptyLinePlaceholder":336},[318,642,643],{"class":320,"line":428},[318,644,645],{},"    # Basic Identity (optional - see \"Extending User\" below for full profiles)\n",[318,647,648],{"class":320,"line":434},[318,649,650],{},"    first_name: Optional[str] = None  # Optional display name\n",[318,652,653],{"class":320,"line":440},[318,654,655],{},"    last_name: Optional[str] = None   # Optional display name\n",[318,657,658],{"class":320,"line":445},[318,659,337],{"emptyLinePlaceholder":336},[318,661,662],{"class":320,"line":451},[318,663,664],{},"    # Organization Binding (EnterpriseRBAC) - See DD-050\n",[318,666,667],{"class":320,"line":456},[318,668,669],{},"    root_entity_id: Optional[UUID] = None  # Root entity this user belongs to\n",[318,671,672],{"class":320,"line":461},[318,673,674],{},"    # NULL for superusers or unassigned users\n",[318,676,677],{"class":320,"line":466},[318,678,679],{},"    # Set automatically on first membership assignment\n",[318,681,682],{"class":320,"line":472},[318,683,684],{},"    # Users can only belong to ONE root entity (organization)\n",[318,686,688],{"class":320,"line":687},28,[318,689,337],{"emptyLinePlaceholder":336},[318,691,693],{"class":320,"line":692},29,[318,694,695],{},"    # Profile\n",[318,697,699],{"class":320,"line":698},30,[318,700,701],{},"    avatar_url: Optional[str] = None\n",[318,703,705],{"class":320,"line":704},31,[318,706,707],{},"    phone: Optional[str] = None\n",[318,709,711],{"class":320,"line":710},32,[318,712,713],{},"    locale: Optional[str] = None        # e.g. \"en-US\"\n",[318,715,717],{"class":320,"line":716},33,[318,718,719],{},"    timezone: Optional[str] = None      # e.g. \"America\u002FNew_York\"\n",[318,721,723],{"class":320,"line":722},34,[318,724,337],{"emptyLinePlaceholder":336},[318,726,728],{"class":320,"line":727},35,[318,729,730],{},"    # Status\n",[318,732,734],{"class":320,"line":733},36,[318,735,736],{},"    status: UserStatus = UserStatus.ACTIVE\n",[318,738,740],{"class":320,"line":739},37,[318,741,742],{},"    suspended_until: Optional[datetime] = None  # Auto-expiry for SUSPENDED status\n",[318,744,746],{"class":320,"line":745},38,[318,747,748],{},"    deleted_at: Optional[datetime] = None       # Timestamp for DELETED status\n",[318,750,752],{"class":320,"line":751},39,[318,753,754],{},"    is_superuser: bool = False\n",[318,756,758],{"class":320,"line":757},40,[318,759,760],{},"    email_verified: bool = False\n",[318,762,764],{"class":320,"line":763},41,[318,765,766],{},"    phone_verified: bool = False\n",[318,768,770],{"class":320,"line":769},42,[318,771,337],{"emptyLinePlaceholder":336},[318,773,775],{"class":320,"line":774},43,[318,776,777],{},"    # Security & Activity Tracking\n",[318,779,781],{"class":320,"line":780},44,[318,782,783],{},"    last_login: Optional[datetime] = None          # Only on email\u002Fpassword or OAuth login\n",[318,785,787],{"class":320,"line":786},45,[318,788,789],{},"    last_activity: Optional[datetime] = None       # Any authenticated action (DD-049)\n",[318,791,793],{"class":320,"line":792},46,[318,794,795],{},"    last_password_change: Optional[datetime] = None\n",[318,797,799],{"class":320,"line":798},47,[318,800,801],{},"    failed_login_attempts: int = 0\n",[318,803,805],{"class":320,"line":804},48,[318,806,807],{},"    locked_until: Optional[datetime] = None\n",[318,809,811],{"class":320,"line":810},49,[318,812,337],{"emptyLinePlaceholder":336},[318,814,816],{"class":320,"line":815},50,[318,817,818],{},"    # Password Reset\n",[318,820,822],{"class":320,"line":821},51,[318,823,824],{},"    password_reset_token: Optional[str] = None\n",[318,826,828],{"class":320,"line":827},52,[318,829,830],{},"    password_reset_expires: Optional[datetime] = None\n",[318,832,834],{"class":320,"line":833},53,[318,835,337],{"emptyLinePlaceholder":336},[318,837,839],{"class":320,"line":838},54,[318,840,841],{},"    # Email Verification\n",[318,843,845],{"class":320,"line":844},55,[318,846,847],{},"    email_verification_token: Optional[str] = None\n",[318,849,851],{"class":320,"line":850},56,[318,852,853],{},"    email_verification_expires: Optional[datetime] = None\n",[318,855,857],{"class":320,"line":856},57,[318,858,337],{"emptyLinePlaceholder":336},[318,860,862],{"class":320,"line":861},58,[318,863,864],{},"    # Invitation\n",[318,866,868],{"class":320,"line":867},59,[318,869,870],{},"    invite_token: Optional[str] = None          # SHA-256 hash of the invite token\n",[318,872,874],{"class":320,"line":873},60,[318,875,876],{},"    invite_token_expires: Optional[datetime] = None\n",[318,878,880],{"class":320,"line":879},61,[318,881,882],{},"    invited_by_id: Optional[UUID] = None        # FK -> users.id\n",[198,884,885,888,889,892,893,892,896,899,900,892,903,226],{},[210,886,887],{},"Relationships",": ",[219,890,891],{},"refresh_tokens",", ",[219,894,895],{},"api_keys",[219,897,898],{},"social_accounts",",\n",[219,901,902],{},"role_memberships",[219,904,905],{},"created_integration_principals",[198,907,908,298,911,914],{},[210,909,910],{},"Constraints & Indexes",[219,912,913],{},"__table_args__","):",[278,916,918],{"className":312,"code":917,"language":314,"meta":286,"style":286},"UniqueConstraint(\"email\", name=\"uq_users_email\")   # also serves as the email index\nIndex(\"ix_users_status\", \"status\")                 # filter by status\nIndex(\"ix_users_root_entity_id\", \"root_entity_id\") # organization scoping\n\n# Partial indexes — reset-password \u002F accept-invite are unauthenticated\n# endpoints that look tokens up by value.\nIndex(\"ix_users_password_reset_token\", \"password_reset_token\",\n      postgresql_where=text(\"password_reset_token IS NOT NULL\"))\nIndex(\"ix_users_invite_token\", \"invite_token\",\n      postgresql_where=text(\"invite_token IS NOT NULL\"))\n",[219,919,920,925,930,935,939,944,949,954,959,964],{"__ignoreMap":286},[318,921,922],{"class":320,"line":321},[318,923,924],{},"UniqueConstraint(\"email\", name=\"uq_users_email\")   # also serves as the email index\n",[318,926,927],{"class":320,"line":327},[318,928,929],{},"Index(\"ix_users_status\", \"status\")                 # filter by status\n",[318,931,932],{"class":320,"line":333},[318,933,934],{},"Index(\"ix_users_root_entity_id\", \"root_entity_id\") # organization scoping\n",[318,936,937],{"class":320,"line":340},[318,938,337],{"emptyLinePlaceholder":336},[318,940,941],{"class":320,"line":346},[318,942,943],{},"# Partial indexes — reset-password \u002F accept-invite are unauthenticated\n",[318,945,946],{"class":320,"line":352},[318,947,948],{},"# endpoints that look tokens up by value.\n",[318,950,951],{"class":320,"line":358},[318,952,953],{},"Index(\"ix_users_password_reset_token\", \"password_reset_token\",\n",[318,955,956],{"class":320,"line":364},[318,957,958],{},"      postgresql_where=text(\"password_reset_token IS NOT NULL\"))\n",[318,960,961],{"class":320,"line":369},[318,962,963],{},"Index(\"ix_users_invite_token\", \"invite_token\",\n",[318,965,966],{"class":320,"line":374},[318,967,968],{},"      postgresql_where=text(\"invite_token IS NOT NULL\"))\n",[198,970,971,503],{},[210,972,973],{},"Key Methods",[278,975,977],{"className":312,"code":976,"language":314,"meta":286,"style":286},"@property\ndef full_name(self) -> str:\n    \"\"\"Get user's full name.\n\n    Returns first_name + last_name if available, otherwise email username.\n    \"\"\"\n    if self.first_name or self.last_name:\n        parts = [p for p in [self.first_name, self.last_name] if p]\n        return \" \".join(parts)\n    return self.email.split(\"@\")[0]\n\n@property\ndef is_locked(self) -> bool:\n    \"\"\"Check if account is locked due to failed login attempts.\"\"\"\n    if not self.locked_until:\n        return False\n    return datetime.now(timezone.utc) \u003C self.locked_until\n\ndef can_authenticate(self) -> bool:\n    \"\"\"Check if user can authenticate.\n\n    Returns True only if:\n    - status == ACTIVE (not suspended, banned, or deleted)\n    - account is not locked (locked_until expired or None)\n\n    See DD-048 for user status semantics.\n    \"\"\"\n    return (\n        self.status == UserStatus.ACTIVE\n        and not self.is_locked\n    )\n",[219,978,979,984,989,994,998,1003,1007,1012,1017,1022,1027,1031,1035,1040,1045,1050,1055,1060,1064,1069,1074,1078,1083,1088,1093,1097,1102,1106,1111,1116,1121],{"__ignoreMap":286},[318,980,981],{"class":320,"line":321},[318,982,983],{},"@property\n",[318,985,986],{"class":320,"line":327},[318,987,988],{},"def full_name(self) -> str:\n",[318,990,991],{"class":320,"line":333},[318,992,993],{},"    \"\"\"Get user's full name.\n",[318,995,996],{"class":320,"line":340},[318,997,337],{"emptyLinePlaceholder":336},[318,999,1000],{"class":320,"line":346},[318,1001,1002],{},"    Returns first_name + last_name if available, otherwise email username.\n",[318,1004,1005],{"class":320,"line":352},[318,1006,580],{},[318,1008,1009],{"class":320,"line":358},[318,1010,1011],{},"    if self.first_name or self.last_name:\n",[318,1013,1014],{"class":320,"line":364},[318,1015,1016],{},"        parts = [p for p in [self.first_name, self.last_name] if p]\n",[318,1018,1019],{"class":320,"line":369},[318,1020,1021],{},"        return \" \".join(parts)\n",[318,1023,1024],{"class":320,"line":374},[318,1025,1026],{},"    return self.email.split(\"@\")[0]\n",[318,1028,1029],{"class":320,"line":380},[318,1030,337],{"emptyLinePlaceholder":336},[318,1032,1033],{"class":320,"line":386},[318,1034,983],{},[318,1036,1037],{"class":320,"line":392},[318,1038,1039],{},"def is_locked(self) -> bool:\n",[318,1041,1042],{"class":320,"line":398},[318,1043,1044],{},"    \"\"\"Check if account is locked due to failed login attempts.\"\"\"\n",[318,1046,1047],{"class":320,"line":404},[318,1048,1049],{},"    if not self.locked_until:\n",[318,1051,1052],{"class":320,"line":410},[318,1053,1054],{},"        return False\n",[318,1056,1057],{"class":320,"line":416},[318,1058,1059],{},"    return datetime.now(timezone.utc) \u003C self.locked_until\n",[318,1061,1062],{"class":320,"line":422},[318,1063,337],{"emptyLinePlaceholder":336},[318,1065,1066],{"class":320,"line":428},[318,1067,1068],{},"def can_authenticate(self) -> bool:\n",[318,1070,1071],{"class":320,"line":434},[318,1072,1073],{},"    \"\"\"Check if user can authenticate.\n",[318,1075,1076],{"class":320,"line":440},[318,1077,337],{"emptyLinePlaceholder":336},[318,1079,1080],{"class":320,"line":445},[318,1081,1082],{},"    Returns True only if:\n",[318,1084,1085],{"class":320,"line":451},[318,1086,1087],{},"    - status == ACTIVE (not suspended, banned, or deleted)\n",[318,1089,1090],{"class":320,"line":456},[318,1091,1092],{},"    - account is not locked (locked_until expired or None)\n",[318,1094,1095],{"class":320,"line":461},[318,1096,337],{"emptyLinePlaceholder":336},[318,1098,1099],{"class":320,"line":466},[318,1100,1101],{},"    See DD-048 for user status semantics.\n",[318,1103,1104],{"class":320,"line":472},[318,1105,580],{},[318,1107,1108],{"class":320,"line":687},[318,1109,1110],{},"    return (\n",[318,1112,1113],{"class":320,"line":692},[318,1114,1115],{},"        self.status == UserStatus.ACTIVE\n",[318,1117,1118],{"class":320,"line":698},[318,1119,1120],{},"        and not self.is_locked\n",[318,1122,1123],{"class":320,"line":704},[318,1124,407],{},[198,1126,1127,503],{},[210,1128,1129],{},"Extending User",[198,1131,1132,1135,1136,892,1139,899,1142,892,1145,892,1148,892,1151,1154],{},[219,1133,1134],{},"User"," already carries the common profile fields - ",[219,1137,1138],{},"first_name",[219,1140,1141],{},"last_name",[219,1143,1144],{},"avatar_url",[219,1146,1147],{},"phone",[219,1149,1150],{},"locale",[219,1152,1153],{},"timezone",". Check that list before adding\nanything.",[198,1156,1157,1158,1161,1162,1165],{},"For business-specific data beyond those fields, the pattern used by the\nlibrary's own example (",[219,1159,1160],{},"examples\u002Fenterprise_rbac\u002Fmodels.py",") is to declare a\n",[210,1163,1164],{},"separate application table in your own app"," that references the user by id:",[278,1167,1169],{"className":312,"code":1168,"language":314,"meta":286,"style":286},"from uuid import UUID\nfrom sqlmodel import Field, SQLModel\n\nclass Lead(SQLModel, table=True):\n    __tablename__ = \"leads\"\n\n    assigned_to: Optional[UUID] = Field(\n        default=None,\n        sa_type=PG_UUID(as_uuid=True),\n        description=\"Agent user_id (optional)\",\n    )\n",[219,1170,1171,1176,1180,1184,1189,1194,1198,1203,1208,1212,1217],{"__ignoreMap":286},[318,1172,1173],{"class":320,"line":321},[318,1174,1175],{},"from uuid import UUID\n",[318,1177,1178],{"class":320,"line":327},[318,1179,361],{},[318,1181,1182],{"class":320,"line":333},[318,1183,337],{"emptyLinePlaceholder":336},[318,1185,1186],{"class":320,"line":340},[318,1187,1188],{},"class Lead(SQLModel, table=True):\n",[318,1190,1191],{"class":320,"line":346},[318,1192,1193],{},"    __tablename__ = \"leads\"\n",[318,1195,1196],{"class":320,"line":352},[318,1197,337],{"emptyLinePlaceholder":336},[318,1199,1200],{"class":320,"line":358},[318,1201,1202],{},"    assigned_to: Optional[UUID] = Field(\n",[318,1204,1205],{"class":320,"line":364},[318,1206,1207],{},"        default=None,\n",[318,1209,1210],{"class":320,"line":369},[318,1211,401],{},[318,1213,1214],{"class":320,"line":374},[318,1215,1216],{},"        description=\"Agent user_id (optional)\",\n",[318,1218,1219],{"class":320,"line":380},[318,1220,407],{},[198,1222,1223,1224,1234,1235,1238],{},"Note that the reference is a ",[210,1225,1226,1227,1230,1231],{},"plain ",[219,1228,1229],{},"UUID"," column with no ",[219,1232,1233],{},"ForeignKey"," - the\napp's tables and the ",[219,1236,1237],{},"outlabs_auth"," tables are migrated independently, so the\nexample deliberately keeps them decoupled and resolves the user in application\ncode.",[198,1240,1241,1242,1245,1246,1248,1249,1251],{},"There is ",[210,1243,1244],{},"no supported subclass-extension hook"," on ",[219,1247,1134],{}," - the library\nconstructs and queries ",[219,1250,1134],{}," directly, so a subclass of it will not be used by\nthe library's own services.",[198,1253,1254,503],{},[210,1255,1256],{},"Example",[278,1258,1260],{"className":312,"code":1259,"language":314,"meta":286,"style":286},"# Create active user\nuser = User(\n    email=\"john@example.com\",\n    hashed_password=hashed_pw,\n    first_name=\"John\",\n    last_name=\"Doe\",\n    status=UserStatus.ACTIVE,\n    email_verified=True,\n)\nsession.add(user)\nawait session.commit()\n\n# Suspend user temporarily (with auto-expiry)\nuser.status = UserStatus.SUSPENDED\nuser.suspended_until = datetime.now(timezone.utc) + timedelta(days=7)\nawait session.commit()\n\n# Soft delete user\nuser.status = UserStatus.DELETED\nuser.deleted_at = datetime.now(timezone.utc)\nawait session.commit()\n",[219,1261,1262,1267,1272,1277,1282,1287,1292,1297,1302,1307,1312,1317,1321,1326,1331,1336,1340,1344,1349,1354,1359],{"__ignoreMap":286},[318,1263,1264],{"class":320,"line":321},[318,1265,1266],{},"# Create active user\n",[318,1268,1269],{"class":320,"line":327},[318,1270,1271],{},"user = User(\n",[318,1273,1274],{"class":320,"line":333},[318,1275,1276],{},"    email=\"john@example.com\",\n",[318,1278,1279],{"class":320,"line":340},[318,1280,1281],{},"    hashed_password=hashed_pw,\n",[318,1283,1284],{"class":320,"line":346},[318,1285,1286],{},"    first_name=\"John\",\n",[318,1288,1289],{"class":320,"line":352},[318,1290,1291],{},"    last_name=\"Doe\",\n",[318,1293,1294],{"class":320,"line":358},[318,1295,1296],{},"    status=UserStatus.ACTIVE,\n",[318,1298,1299],{"class":320,"line":364},[318,1300,1301],{},"    email_verified=True,\n",[318,1303,1304],{"class":320,"line":369},[318,1305,1306],{},")\n",[318,1308,1309],{"class":320,"line":374},[318,1310,1311],{},"session.add(user)\n",[318,1313,1314],{"class":320,"line":380},[318,1315,1316],{},"await session.commit()\n",[318,1318,1319],{"class":320,"line":386},[318,1320,337],{"emptyLinePlaceholder":336},[318,1322,1323],{"class":320,"line":392},[318,1324,1325],{},"# Suspend user temporarily (with auto-expiry)\n",[318,1327,1328],{"class":320,"line":398},[318,1329,1330],{},"user.status = UserStatus.SUSPENDED\n",[318,1332,1333],{"class":320,"line":404},[318,1334,1335],{},"user.suspended_until = datetime.now(timezone.utc) + timedelta(days=7)\n",[318,1337,1338],{"class":320,"line":410},[318,1339,1316],{},[318,1341,1342],{"class":320,"line":416},[318,1343,337],{"emptyLinePlaceholder":336},[318,1345,1346],{"class":320,"line":422},[318,1347,1348],{},"# Soft delete user\n",[318,1350,1351],{"class":320,"line":428},[318,1352,1353],{},"user.status = UserStatus.DELETED\n",[318,1355,1356],{"class":320,"line":434},[318,1357,1358],{},"user.deleted_at = datetime.now(timezone.utc)\n",[318,1360,1361],{"class":320,"line":440},[318,1362,1316],{},[271,1364],{},[539,1366,1368],{"id":1367},"_2-role","2. Role",[198,1370,1371,548,1374],{},[210,1372,1373],{},"Roles with permissions and optional ABAC conditions.",[219,1375,1376],{},"roles",[198,1378,1379,1380,1382],{},"Unlike the other models here, a role's permissions and conditions are ",[210,1381,305],{},"\ncolumns - they are normalized into their own tables:",[1384,1385,1386,1402],"table",{},[1387,1388,1389],"thead",{},[1390,1391,1392,1396,1399],"tr",{},[1393,1394,1395],"th",{},"Related table",[1393,1397,1398],{},"Model",[1393,1400,1401],{},"Holds",[1403,1404,1405,1421,1436,1451,1466],"tbody",{},[1390,1406,1407,1413,1418],{},[1408,1409,1410],"td",{},[219,1411,1412],{},"role_permissions",[1408,1414,1415],{},[219,1416,1417],{},"RolePermission",[1408,1419,1420],{},"Role -> Permission junction",[1390,1422,1423,1428,1433],{},[1408,1424,1425],{},[219,1426,1427],{},"role_conditions",[1408,1429,1430],{},[219,1431,1432],{},"RoleCondition",[1408,1434,1435],{},"ABAC conditions on the role",[1390,1437,1438,1443,1448],{},[1408,1439,1440],{},[219,1441,1442],{},"role_entity_type_permissions",[1408,1444,1445],{},[219,1446,1447],{},"RoleEntityTypePermission",[1408,1449,1450],{},"Context-aware (per entity type) permissions",[1390,1452,1453,1458,1463],{},[1408,1454,1455],{},[219,1456,1457],{},"condition_groups",[1408,1459,1460],{},[219,1461,1462],{},"ConditionGroup",[1408,1464,1465],{},"AND\u002FOR grouping of conditions",[1390,1467,1468,1473,1478],{},[1408,1469,1470],{},[219,1471,1472],{},"role_definition_history",[1408,1474,1475],{},[219,1476,1477],{},"RoleDefinitionHistory",[1408,1479,1480],{},"Definition change history",[278,1482,1484],{"className":312,"code":1483,"language":314,"meta":286,"style":286},"class Role(BaseModel, table=True):\n    __tablename__ = \"roles\"\n\n    # Identity\n    name: str                        # System name (lowercase, e.g. \"admin\")\n    display_name: str                # Human-readable name\n    description: Optional[str] = None\n\n    # Root Entity Scope (EnterpriseRBAC) - See DD-050\n    root_entity_id: Optional[UUID] = None  # FK -> entities.id (SET NULL)\n    # NULL = system-wide role available everywhere\n\n    # Configuration\n    is_system_role: bool = False     # System roles cannot be modified\u002Fdeleted\n    is_global: bool = False          # Can be assigned anywhere in hierarchy\n    status: DefinitionStatus = DefinitionStatus.ACTIVE\n\n    # Entity-Local Role Configuration (DD-053)\n    scope_entity_id: Optional[UUID] = None  # FK -> entities.id (CASCADE)\n    scope: RoleScope = RoleScope.HIERARCHY  # entity_only | hierarchy\n    is_auto_assigned: bool = False   # Auto-assigned to all members within scope\n    assignable_at_types: List[str] = []  # Empty = no entity-type restriction\n\n    # Relationships\n    permissions: List[\"Permission\"]  # via role_permissions junction\n    conditions: List[\"RoleCondition\"]\n    entity_type_permissions: List[\"RoleEntityTypePermission\"]\n",[219,1485,1486,1491,1496,1500,1505,1510,1515,1520,1524,1529,1534,1539,1543,1548,1553,1558,1563,1567,1572,1577,1582,1587,1592,1596,1601,1606,1611],{"__ignoreMap":286},[318,1487,1488],{"class":320,"line":321},[318,1489,1490],{},"class Role(BaseModel, table=True):\n",[318,1492,1493],{"class":320,"line":327},[318,1494,1495],{},"    __tablename__ = \"roles\"\n",[318,1497,1498],{"class":320,"line":333},[318,1499,337],{"emptyLinePlaceholder":336},[318,1501,1502],{"class":320,"line":340},[318,1503,1504],{},"    # Identity\n",[318,1506,1507],{"class":320,"line":346},[318,1508,1509],{},"    name: str                        # System name (lowercase, e.g. \"admin\")\n",[318,1511,1512],{"class":320,"line":352},[318,1513,1514],{},"    display_name: str                # Human-readable name\n",[318,1516,1517],{"class":320,"line":358},[318,1518,1519],{},"    description: Optional[str] = None\n",[318,1521,1522],{"class":320,"line":364},[318,1523,337],{"emptyLinePlaceholder":336},[318,1525,1526],{"class":320,"line":369},[318,1527,1528],{},"    # Root Entity Scope (EnterpriseRBAC) - See DD-050\n",[318,1530,1531],{"class":320,"line":374},[318,1532,1533],{},"    root_entity_id: Optional[UUID] = None  # FK -> entities.id (SET NULL)\n",[318,1535,1536],{"class":320,"line":380},[318,1537,1538],{},"    # NULL = system-wide role available everywhere\n",[318,1540,1541],{"class":320,"line":386},[318,1542,337],{"emptyLinePlaceholder":336},[318,1544,1545],{"class":320,"line":392},[318,1546,1547],{},"    # Configuration\n",[318,1549,1550],{"class":320,"line":398},[318,1551,1552],{},"    is_system_role: bool = False     # System roles cannot be modified\u002Fdeleted\n",[318,1554,1555],{"class":320,"line":404},[318,1556,1557],{},"    is_global: bool = False          # Can be assigned anywhere in hierarchy\n",[318,1559,1560],{"class":320,"line":410},[318,1561,1562],{},"    status: DefinitionStatus = DefinitionStatus.ACTIVE\n",[318,1564,1565],{"class":320,"line":416},[318,1566,337],{"emptyLinePlaceholder":336},[318,1568,1569],{"class":320,"line":422},[318,1570,1571],{},"    # Entity-Local Role Configuration (DD-053)\n",[318,1573,1574],{"class":320,"line":428},[318,1575,1576],{},"    scope_entity_id: Optional[UUID] = None  # FK -> entities.id (CASCADE)\n",[318,1578,1579],{"class":320,"line":434},[318,1580,1581],{},"    scope: RoleScope = RoleScope.HIERARCHY  # entity_only | hierarchy\n",[318,1583,1584],{"class":320,"line":440},[318,1585,1586],{},"    is_auto_assigned: bool = False   # Auto-assigned to all members within scope\n",[318,1588,1589],{"class":320,"line":445},[318,1590,1591],{},"    assignable_at_types: List[str] = []  # Empty = no entity-type restriction\n",[318,1593,1594],{"class":320,"line":451},[318,1595,337],{"emptyLinePlaceholder":336},[318,1597,1598],{"class":320,"line":456},[318,1599,1600],{},"    # Relationships\n",[318,1602,1603],{"class":320,"line":461},[318,1604,1605],{},"    permissions: List[\"Permission\"]  # via role_permissions junction\n",[318,1607,1608],{"class":320,"line":466},[318,1609,1610],{},"    conditions: List[\"RoleCondition\"]\n",[318,1612,1613],{"class":320,"line":472},[318,1614,1615],{},"    entity_type_permissions: List[\"RoleEntityTypePermission\"]\n",[198,1617,1618,503],{},[210,1619,1620],{},"Indexes",[278,1622,1624],{"className":312,"code":1623,"language":314,"meta":286,"style":286},"Index(\"ix_roles_name\", \"name\")\nIndex(\"ix_roles_is_global\", \"is_global\")\nIndex(\"ix_roles_status\", \"status\")\nIndex(\"ix_roles_root_entity_id\", \"root_entity_id\")\nIndex(\"ix_roles_scope_entity_id\", \"scope_entity_id\")\nIndex(\"ix_roles_is_auto_assigned\", \"is_auto_assigned\")\n",[219,1625,1626,1631,1636,1641,1646,1651],{"__ignoreMap":286},[318,1627,1628],{"class":320,"line":321},[318,1629,1630],{},"Index(\"ix_roles_name\", \"name\")\n",[318,1632,1633],{"class":320,"line":327},[318,1634,1635],{},"Index(\"ix_roles_is_global\", \"is_global\")\n",[318,1637,1638],{"class":320,"line":333},[318,1639,1640],{},"Index(\"ix_roles_status\", \"status\")\n",[318,1642,1643],{"class":320,"line":340},[318,1644,1645],{},"Index(\"ix_roles_root_entity_id\", \"root_entity_id\")\n",[318,1647,1648],{"class":320,"line":346},[318,1649,1650],{},"Index(\"ix_roles_scope_entity_id\", \"scope_entity_id\")\n",[318,1652,1653],{"class":320,"line":352},[318,1654,1655],{},"Index(\"ix_roles_is_auto_assigned\", \"is_auto_assigned\")\n",[198,1657,1658,1659,1662,1663,1666],{},"Note: ",[219,1660,1661],{},"name"," is ",[210,1664,1665],{},"indexed but not unique"," at the table level - the same role\nname can exist in different root entities.",[198,1668,1669,503],{},[210,1670,973],{},[278,1672,1674],{"className":312,"code":1673,"language":314,"meta":286,"style":286},"def get_permission_names(self) -> List[str]:\n    \"\"\"Get list of permission names assigned to this role.\"\"\"\n\ndef has_conditions(self) -> bool:\n    \"\"\"Check if role has ABAC conditions.\"\"\"\n\ndef is_entity_local(self) -> bool:\n    \"\"\"Check if this is an entity-local role (has scope_entity_id).\"\"\"\n\ndef is_hierarchy_scoped(self) -> bool:\n    \"\"\"Check if role permissions cascade to descendants.\"\"\"\n\ndef is_entity_only_scoped(self) -> bool:\n    \"\"\"Check if role permissions are limited to the scope entity only.\"\"\"\n\ndef can_grant_permissions(self) -> bool:\n    \"\"\"Only active role definitions grant access.\"\"\"\n",[219,1675,1676,1681,1686,1690,1695,1700,1704,1709,1714,1718,1723,1728,1732,1737,1742,1746,1751],{"__ignoreMap":286},[318,1677,1678],{"class":320,"line":321},[318,1679,1680],{},"def get_permission_names(self) -> List[str]:\n",[318,1682,1683],{"class":320,"line":327},[318,1684,1685],{},"    \"\"\"Get list of permission names assigned to this role.\"\"\"\n",[318,1687,1688],{"class":320,"line":333},[318,1689,337],{"emptyLinePlaceholder":336},[318,1691,1692],{"class":320,"line":340},[318,1693,1694],{},"def has_conditions(self) -> bool:\n",[318,1696,1697],{"class":320,"line":346},[318,1698,1699],{},"    \"\"\"Check if role has ABAC conditions.\"\"\"\n",[318,1701,1702],{"class":320,"line":352},[318,1703,337],{"emptyLinePlaceholder":336},[318,1705,1706],{"class":320,"line":358},[318,1707,1708],{},"def is_entity_local(self) -> bool:\n",[318,1710,1711],{"class":320,"line":364},[318,1712,1713],{},"    \"\"\"Check if this is an entity-local role (has scope_entity_id).\"\"\"\n",[318,1715,1716],{"class":320,"line":369},[318,1717,337],{"emptyLinePlaceholder":336},[318,1719,1720],{"class":320,"line":374},[318,1721,1722],{},"def is_hierarchy_scoped(self) -> bool:\n",[318,1724,1725],{"class":320,"line":380},[318,1726,1727],{},"    \"\"\"Check if role permissions cascade to descendants.\"\"\"\n",[318,1729,1730],{"class":320,"line":386},[318,1731,337],{"emptyLinePlaceholder":336},[318,1733,1734],{"class":320,"line":392},[318,1735,1736],{},"def is_entity_only_scoped(self) -> bool:\n",[318,1738,1739],{"class":320,"line":398},[318,1740,1741],{},"    \"\"\"Check if role permissions are limited to the scope entity only.\"\"\"\n",[318,1743,1744],{"class":320,"line":404},[318,1745,337],{"emptyLinePlaceholder":336},[318,1747,1748],{"class":320,"line":410},[318,1749,1750],{},"def can_grant_permissions(self) -> bool:\n",[318,1752,1753],{"class":320,"line":416},[318,1754,1755],{},"    \"\"\"Only active role definitions grant access.\"\"\"\n",[198,1757,1758,503],{},[210,1759,1256],{},[278,1761,1763],{"className":312,"code":1762,"language":314,"meta":286,"style":286},"# Simple role\neditor = Role(\n    name=\"editor\",\n    display_name=\"Editor\",\n)\nsession.add(editor)\nawait session.flush()  # populate editor.id\n\n# Permissions attach via the role_permissions junction table\nsession.add(RolePermission(role_id=editor.id, permission_id=post_create.id))\nsession.add(RolePermission(role_id=editor.id, permission_id=post_update.id))\n\n# Context-aware permissions attach per entity type\nsession.add(\n    RoleEntityTypePermission(\n        role_id=manager.id,\n        entity_type=\"department\",\n        permission_id=budget_approve.id,\n    )\n)\n\n# ABAC conditions are rows in role_conditions\nsession.add(\n    RoleCondition(\n        role_id=budget_approver.id,\n        attribute=\"resource.amount\",\n        operator=ConditionOperator.LESS_THAN,\n        value=\"100000\",\n        value_type=\"int\",\n    )\n)\nawait session.commit()\n",[219,1764,1765,1770,1775,1780,1785,1789,1794,1799,1803,1808,1813,1818,1822,1827,1832,1837,1842,1847,1852,1856,1860,1864,1869,1873,1878,1883,1888,1893,1898,1903,1907,1911],{"__ignoreMap":286},[318,1766,1767],{"class":320,"line":321},[318,1768,1769],{},"# Simple role\n",[318,1771,1772],{"class":320,"line":327},[318,1773,1774],{},"editor = Role(\n",[318,1776,1777],{"class":320,"line":333},[318,1778,1779],{},"    name=\"editor\",\n",[318,1781,1782],{"class":320,"line":340},[318,1783,1784],{},"    display_name=\"Editor\",\n",[318,1786,1787],{"class":320,"line":346},[318,1788,1306],{},[318,1790,1791],{"class":320,"line":352},[318,1792,1793],{},"session.add(editor)\n",[318,1795,1796],{"class":320,"line":358},[318,1797,1798],{},"await session.flush()  # populate editor.id\n",[318,1800,1801],{"class":320,"line":364},[318,1802,337],{"emptyLinePlaceholder":336},[318,1804,1805],{"class":320,"line":369},[318,1806,1807],{},"# Permissions attach via the role_permissions junction table\n",[318,1809,1810],{"class":320,"line":374},[318,1811,1812],{},"session.add(RolePermission(role_id=editor.id, permission_id=post_create.id))\n",[318,1814,1815],{"class":320,"line":380},[318,1816,1817],{},"session.add(RolePermission(role_id=editor.id, permission_id=post_update.id))\n",[318,1819,1820],{"class":320,"line":386},[318,1821,337],{"emptyLinePlaceholder":336},[318,1823,1824],{"class":320,"line":392},[318,1825,1826],{},"# Context-aware permissions attach per entity type\n",[318,1828,1829],{"class":320,"line":398},[318,1830,1831],{},"session.add(\n",[318,1833,1834],{"class":320,"line":404},[318,1835,1836],{},"    RoleEntityTypePermission(\n",[318,1838,1839],{"class":320,"line":410},[318,1840,1841],{},"        role_id=manager.id,\n",[318,1843,1844],{"class":320,"line":416},[318,1845,1846],{},"        entity_type=\"department\",\n",[318,1848,1849],{"class":320,"line":422},[318,1850,1851],{},"        permission_id=budget_approve.id,\n",[318,1853,1854],{"class":320,"line":428},[318,1855,407],{},[318,1857,1858],{"class":320,"line":434},[318,1859,1306],{},[318,1861,1862],{"class":320,"line":440},[318,1863,337],{"emptyLinePlaceholder":336},[318,1865,1866],{"class":320,"line":445},[318,1867,1868],{},"# ABAC conditions are rows in role_conditions\n",[318,1870,1871],{"class":320,"line":451},[318,1872,1831],{},[318,1874,1875],{"class":320,"line":456},[318,1876,1877],{},"    RoleCondition(\n",[318,1879,1880],{"class":320,"line":461},[318,1881,1882],{},"        role_id=budget_approver.id,\n",[318,1884,1885],{"class":320,"line":466},[318,1886,1887],{},"        attribute=\"resource.amount\",\n",[318,1889,1890],{"class":320,"line":472},[318,1891,1892],{},"        operator=ConditionOperator.LESS_THAN,\n",[318,1894,1895],{"class":320,"line":687},[318,1896,1897],{},"        value=\"100000\",\n",[318,1899,1900],{"class":320,"line":692},[318,1901,1902],{},"        value_type=\"int\",\n",[318,1904,1905],{"class":320,"line":698},[318,1906,407],{},[318,1908,1909],{"class":320,"line":704},[318,1910,1306],{},[318,1912,1913],{"class":320,"line":710},[318,1914,1316],{},[198,1916,1917,1918,1921,1922,1925,1926,1929],{},"Note that ",[219,1919,1920],{},"RoleCondition.value"," is a ",[210,1923,1924],{},"string column"," with a companion\n",[219,1927,1928],{},"value_type"," discriminator - values are serialized, not stored natively typed.",[271,1931],{},[539,1933,1935],{"id":1934},"_3-permission","3. Permission",[198,1937,1938,548,1941],{},[210,1939,1940],{},"Permission definitions.",[219,1942,1943],{},"permissions",[278,1945,1947],{"className":312,"code":1946,"language":314,"meta":286,"style":286},"class Permission(BaseModel, table=True):\n    __tablename__ = \"permissions\"\n\n    name: str                        # e.g. \"post:create\"\n    display_name: str\n    description: Optional[str] = None\n\n    resource: Optional[str] = None   # e.g. \"post\"\n    action: Optional[str] = None     # e.g. \"create\"\n    scope: Optional[str] = None\n\n    is_system: bool = False\n    status: DefinitionStatus = DefinitionStatus.ACTIVE\n    is_active: bool = True\n\n    # Relationships\n    tags: List[\"PermissionTag\"]              # via permission_tag_links\n    conditions: List[\"PermissionCondition\"]\n",[219,1948,1949,1954,1959,1963,1968,1973,1977,1981,1986,1991,1996,2000,2005,2009,2014,2018,2022,2027],{"__ignoreMap":286},[318,1950,1951],{"class":320,"line":321},[318,1952,1953],{},"class Permission(BaseModel, table=True):\n",[318,1955,1956],{"class":320,"line":327},[318,1957,1958],{},"    __tablename__ = \"permissions\"\n",[318,1960,1961],{"class":320,"line":333},[318,1962,337],{"emptyLinePlaceholder":336},[318,1964,1965],{"class":320,"line":340},[318,1966,1967],{},"    name: str                        # e.g. \"post:create\"\n",[318,1969,1970],{"class":320,"line":346},[318,1971,1972],{},"    display_name: str\n",[318,1974,1975],{"class":320,"line":352},[318,1976,1519],{},[318,1978,1979],{"class":320,"line":358},[318,1980,337],{"emptyLinePlaceholder":336},[318,1982,1983],{"class":320,"line":364},[318,1984,1985],{},"    resource: Optional[str] = None   # e.g. \"post\"\n",[318,1987,1988],{"class":320,"line":369},[318,1989,1990],{},"    action: Optional[str] = None     # e.g. \"create\"\n",[318,1992,1993],{"class":320,"line":374},[318,1994,1995],{},"    scope: Optional[str] = None\n",[318,1997,1998],{"class":320,"line":380},[318,1999,337],{"emptyLinePlaceholder":336},[318,2001,2002],{"class":320,"line":386},[318,2003,2004],{},"    is_system: bool = False\n",[318,2006,2007],{"class":320,"line":392},[318,2008,1562],{},[318,2010,2011],{"class":320,"line":398},[318,2012,2013],{},"    is_active: bool = True\n",[318,2015,2016],{"class":320,"line":404},[318,2017,337],{"emptyLinePlaceholder":336},[318,2019,2020],{"class":320,"line":410},[318,2021,1600],{},[318,2023,2024],{"class":320,"line":416},[318,2025,2026],{},"    tags: List[\"PermissionTag\"]              # via permission_tag_links\n",[318,2028,2029],{"class":320,"line":422},[318,2030,2031],{},"    conditions: List[\"PermissionCondition\"]\n",[198,2033,2034,503],{},[210,2035,910],{},[278,2037,2039],{"className":312,"code":2038,"language":314,"meta":286,"style":286},"UniqueConstraint(\"name\", name=\"uq_permissions_name\")\nIndex(\"ix_permissions_resource\", \"resource\")\nIndex(\"ix_permissions_is_system\", \"is_system\")\nIndex(\"ix_permissions_status\", \"status\")\nIndex(\"ix_permissions_is_active\", \"is_active\")\n",[219,2040,2041,2046,2051,2056,2061],{"__ignoreMap":286},[318,2042,2043],{"class":320,"line":321},[318,2044,2045],{},"UniqueConstraint(\"name\", name=\"uq_permissions_name\")\n",[318,2047,2048],{"class":320,"line":327},[318,2049,2050],{},"Index(\"ix_permissions_resource\", \"resource\")\n",[318,2052,2053],{"class":320,"line":333},[318,2054,2055],{},"Index(\"ix_permissions_is_system\", \"is_system\")\n",[318,2057,2058],{"class":320,"line":340},[318,2059,2060],{},"Index(\"ix_permissions_status\", \"status\")\n",[318,2062,2063],{"class":320,"line":346},[318,2064,2065],{},"Index(\"ix_permissions_is_active\", \"is_active\")\n",[198,2067,2068,888,2071,2074,2075,2078,2079,2082,2083,2086],{},[210,2069,2070],{},"Related tables",[219,2072,2073],{},"permission_tags"," \u002F ",[219,2076,2077],{},"permission_tag_links"," (tagging),\n",[219,2080,2081],{},"permission_conditions"," (ABAC conditions on the permission itself), and\n",[219,2084,2085],{},"permission_definition_history"," (definition change history).",[198,2088,2089,888,2092,2095,2096,2098],{},[210,2090,2091],{},"Note",[219,2093,2094],{},"Permission"," rows are real - roles reference them by id through the\n",[219,2097,1412],{}," junction, not by string.",[271,2100],{},[539,2102,2104],{"id":2103},"_4-refreshtoken","4. RefreshToken",[198,2106,2107],{},[210,2108,2109],{},"Refresh tokens for JWT authentication.",[198,2111,2112,2114,2115,2118],{},[210,2113,2091],{},": Refresh token JWTs now include a ",[219,2116,2117],{},"jti"," (JWT ID) claim to ensure uniqueness and prevent token collisions when multiple sessions are created simultaneously. This JTI is not stored in the database - only the hash of the full JWT is stored.",[278,2120,2122],{"className":312,"code":2121,"language":314,"meta":286,"style":286},"class RefreshToken(BaseModel, table=True):\n    __tablename__ = \"refresh_tokens\"\n\n    # User\n    user_id: UUID                    # FK -> users.id (CASCADE)\n\n    # Token (hashed)\n    token_hash: str                  # SHA-256 hash of the token (never the plain token)\n\n    # Rotation family\n    family_id: UUID                  # Family used to detect replay after rotation\n    replaced_by_token_id: Optional[UUID] = None  # FK -> refresh_tokens.id (SET NULL)\n\n    # Expiration\n    expires_at: datetime\n\n    # Revocation\n    is_revoked: bool = False\n    revoked_at: Optional[datetime] = None\n    revoked_reason: Optional[str] = None  # logout, security, password_change\n\n    # Device\u002FSession info\n    device_name: Optional[str] = None  # e.g. \"iPhone 14 Pro\"\n    device_fingerprint: Optional[str] = None  # Hashed device id for session binding\n    ip_address: Optional[str] = None\n    user_agent: Optional[str] = None\n\n    # Usage tracking\n    last_used_at: Optional[datetime] = None\n    usage_count: int = 0\n\n    # Relationship\n    user: \"User\"\n",[219,2123,2124,2129,2134,2138,2143,2148,2152,2157,2162,2166,2171,2176,2181,2185,2190,2195,2199,2204,2209,2214,2219,2223,2228,2233,2238,2243,2248,2252,2257,2262,2267,2271,2276],{"__ignoreMap":286},[318,2125,2126],{"class":320,"line":321},[318,2127,2128],{},"class RefreshToken(BaseModel, table=True):\n",[318,2130,2131],{"class":320,"line":327},[318,2132,2133],{},"    __tablename__ = \"refresh_tokens\"\n",[318,2135,2136],{"class":320,"line":333},[318,2137,337],{"emptyLinePlaceholder":336},[318,2139,2140],{"class":320,"line":340},[318,2141,2142],{},"    # User\n",[318,2144,2145],{"class":320,"line":346},[318,2146,2147],{},"    user_id: UUID                    # FK -> users.id (CASCADE)\n",[318,2149,2150],{"class":320,"line":352},[318,2151,337],{"emptyLinePlaceholder":336},[318,2153,2154],{"class":320,"line":358},[318,2155,2156],{},"    # Token (hashed)\n",[318,2158,2159],{"class":320,"line":364},[318,2160,2161],{},"    token_hash: str                  # SHA-256 hash of the token (never the plain token)\n",[318,2163,2164],{"class":320,"line":369},[318,2165,337],{"emptyLinePlaceholder":336},[318,2167,2168],{"class":320,"line":374},[318,2169,2170],{},"    # Rotation family\n",[318,2172,2173],{"class":320,"line":380},[318,2174,2175],{},"    family_id: UUID                  # Family used to detect replay after rotation\n",[318,2177,2178],{"class":320,"line":386},[318,2179,2180],{},"    replaced_by_token_id: Optional[UUID] = None  # FK -> refresh_tokens.id (SET NULL)\n",[318,2182,2183],{"class":320,"line":392},[318,2184,337],{"emptyLinePlaceholder":336},[318,2186,2187],{"class":320,"line":398},[318,2188,2189],{},"    # Expiration\n",[318,2191,2192],{"class":320,"line":404},[318,2193,2194],{},"    expires_at: datetime\n",[318,2196,2197],{"class":320,"line":410},[318,2198,337],{"emptyLinePlaceholder":336},[318,2200,2201],{"class":320,"line":416},[318,2202,2203],{},"    # Revocation\n",[318,2205,2206],{"class":320,"line":422},[318,2207,2208],{},"    is_revoked: bool = False\n",[318,2210,2211],{"class":320,"line":428},[318,2212,2213],{},"    revoked_at: Optional[datetime] = None\n",[318,2215,2216],{"class":320,"line":434},[318,2217,2218],{},"    revoked_reason: Optional[str] = None  # logout, security, password_change\n",[318,2220,2221],{"class":320,"line":440},[318,2222,337],{"emptyLinePlaceholder":336},[318,2224,2225],{"class":320,"line":445},[318,2226,2227],{},"    # Device\u002FSession info\n",[318,2229,2230],{"class":320,"line":451},[318,2231,2232],{},"    device_name: Optional[str] = None  # e.g. \"iPhone 14 Pro\"\n",[318,2234,2235],{"class":320,"line":456},[318,2236,2237],{},"    device_fingerprint: Optional[str] = None  # Hashed device id for session binding\n",[318,2239,2240],{"class":320,"line":461},[318,2241,2242],{},"    ip_address: Optional[str] = None\n",[318,2244,2245],{"class":320,"line":466},[318,2246,2247],{},"    user_agent: Optional[str] = None\n",[318,2249,2250],{"class":320,"line":472},[318,2251,337],{"emptyLinePlaceholder":336},[318,2253,2254],{"class":320,"line":687},[318,2255,2256],{},"    # Usage tracking\n",[318,2258,2259],{"class":320,"line":692},[318,2260,2261],{},"    last_used_at: Optional[datetime] = None\n",[318,2263,2264],{"class":320,"line":698},[318,2265,2266],{},"    usage_count: int = 0\n",[318,2268,2269],{"class":320,"line":704},[318,2270,337],{"emptyLinePlaceholder":336},[318,2272,2273],{"class":320,"line":710},[318,2274,2275],{},"    # Relationship\n",[318,2277,2278],{"class":320,"line":716},[318,2279,2280],{},"    user: \"User\"\n",[198,2282,2283,2286,2287,2290,2291,2294],{},[210,2284,2285],{},"Rotation families",": on refresh, the old token is marked revoked and points at\nits replacement via ",[219,2288,2289],{},"replaced_by_token_id",", while both share a ",[219,2292,2293],{},"family_id",".\nRe-use of an already-rotated token is therefore detectable as replay across the\nwhole family.",[198,2296,2297,503],{},[210,2298,910],{},[278,2300,2302],{"className":312,"code":2301,"language":314,"meta":286,"style":286},"UniqueConstraint(\"token_hash\", name=\"uq_refresh_tokens_hash\")\nIndex(\"ix_refresh_tokens_user_id\", \"user_id\")\nIndex(\"ix_refresh_tokens_expires_at\", \"expires_at\")   # cleanup expired tokens\nIndex(\"ix_refresh_tokens_is_revoked\", \"is_revoked\")\nIndex(\"ix_refresh_tokens_device\", \"device_fingerprint\")\nIndex(\"ix_refresh_tokens_family_id\", \"family_id\")\n",[219,2303,2304,2309,2314,2319,2324,2329],{"__ignoreMap":286},[318,2305,2306],{"class":320,"line":321},[318,2307,2308],{},"UniqueConstraint(\"token_hash\", name=\"uq_refresh_tokens_hash\")\n",[318,2310,2311],{"class":320,"line":327},[318,2312,2313],{},"Index(\"ix_refresh_tokens_user_id\", \"user_id\")\n",[318,2315,2316],{"class":320,"line":333},[318,2317,2318],{},"Index(\"ix_refresh_tokens_expires_at\", \"expires_at\")   # cleanup expired tokens\n",[318,2320,2321],{"class":320,"line":340},[318,2322,2323],{},"Index(\"ix_refresh_tokens_is_revoked\", \"is_revoked\")\n",[318,2325,2326],{"class":320,"line":346},[318,2327,2328],{},"Index(\"ix_refresh_tokens_device\", \"device_fingerprint\")\n",[318,2330,2331],{"class":320,"line":352},[318,2332,2333],{},"Index(\"ix_refresh_tokens_family_id\", \"family_id\")\n",[198,2335,2336,503],{},[210,2337,973],{},[278,2339,2341],{"className":312,"code":2340,"language":314,"meta":286,"style":286},"def is_valid(self) -> bool:\n    \"\"\"Check if token is valid (not revoked and not expired).\"\"\"\n\ndef is_expired(self) -> bool:\n    \"\"\"Check if token has expired.\"\"\"\n\ndef revoke(self, reason: str = \"manual\") -> None:\n    \"\"\"Mark token as revoked (sets revoked_at and revoked_reason).\"\"\"\n\ndef record_usage(self) -> None:\n    \"\"\"Record token usage (bumps last_used_at and usage_count).\"\"\"\n",[219,2342,2343,2348,2353,2357,2362,2367,2371,2376,2381,2385,2390],{"__ignoreMap":286},[318,2344,2345],{"class":320,"line":321},[318,2346,2347],{},"def is_valid(self) -> bool:\n",[318,2349,2350],{"class":320,"line":327},[318,2351,2352],{},"    \"\"\"Check if token is valid (not revoked and not expired).\"\"\"\n",[318,2354,2355],{"class":320,"line":333},[318,2356,337],{"emptyLinePlaceholder":336},[318,2358,2359],{"class":320,"line":340},[318,2360,2361],{},"def is_expired(self) -> bool:\n",[318,2363,2364],{"class":320,"line":346},[318,2365,2366],{},"    \"\"\"Check if token has expired.\"\"\"\n",[318,2368,2369],{"class":320,"line":352},[318,2370,337],{"emptyLinePlaceholder":336},[318,2372,2373],{"class":320,"line":358},[318,2374,2375],{},"def revoke(self, reason: str = \"manual\") -> None:\n",[318,2377,2378],{"class":320,"line":364},[318,2379,2380],{},"    \"\"\"Mark token as revoked (sets revoked_at and revoked_reason).\"\"\"\n",[318,2382,2383],{"class":320,"line":369},[318,2384,337],{"emptyLinePlaceholder":336},[318,2386,2387],{"class":320,"line":374},[318,2388,2389],{},"def record_usage(self) -> None:\n",[318,2391,2392],{"class":320,"line":380},[318,2393,2394],{},"    \"\"\"Record token usage (bumps last_used_at and usage_count).\"\"\"\n",[198,2396,2397,503],{},[210,2398,1256],{},[278,2400,2402],{"className":312,"code":2401,"language":314,"meta":286,"style":286},"refresh_token = RefreshToken(\n    user_id=user.id,\n    token_hash=hashed_token,\n    expires_at=datetime.now(timezone.utc) + timedelta(days=30),\n    device_name=\"Chrome on MacOS\",\n    ip_address=\"192.168.1.100\",\n)\nsession.add(refresh_token)\nawait session.commit()\n",[219,2403,2404,2409,2414,2419,2424,2429,2434,2438,2443],{"__ignoreMap":286},[318,2405,2406],{"class":320,"line":321},[318,2407,2408],{},"refresh_token = RefreshToken(\n",[318,2410,2411],{"class":320,"line":327},[318,2412,2413],{},"    user_id=user.id,\n",[318,2415,2416],{"class":320,"line":333},[318,2417,2418],{},"    token_hash=hashed_token,\n",[318,2420,2421],{"class":320,"line":340},[318,2422,2423],{},"    expires_at=datetime.now(timezone.utc) + timedelta(days=30),\n",[318,2425,2426],{"class":320,"line":346},[318,2427,2428],{},"    device_name=\"Chrome on MacOS\",\n",[318,2430,2431],{"class":320,"line":352},[318,2432,2433],{},"    ip_address=\"192.168.1.100\",\n",[318,2435,2436],{"class":320,"line":358},[318,2437,1306],{},[318,2439,2440],{"class":320,"line":364},[318,2441,2442],{},"session.add(refresh_token)\n",[318,2444,2445],{"class":320,"line":369},[318,2446,1316],{},[271,2448],{},[539,2450,2452],{"id":2451},"_5-apikey","5. APIKey",[198,2454,2455,548,2458],{},[210,2456,2457],{},"API keys for programmatic authentication.",[219,2459,895],{},[198,2461,2462,2463,2466],{},"Scopes and IP whitelist entries are ",[210,2464,2465],{},"child tables",", not array columns:",[1384,2468,2469,2479],{},[1387,2470,2471],{},[1390,2472,2473,2475,2477],{},[1393,2474,1395],{},[1393,2476,1398],{},[1393,2478,1401],{},[1403,2480,2481,2496,2511],{},[1390,2482,2483,2488,2493],{},[1408,2484,2485],{},[219,2486,2487],{},"api_key_scopes",[1408,2489,2490],{},[219,2491,2492],{},"APIKeyScope",[1408,2494,2495],{},"One row per granted scope",[1390,2497,2498,2503,2508],{},[1408,2499,2500],{},[219,2501,2502],{},"api_key_ip_whitelist",[1408,2504,2505],{},[219,2506,2507],{},"APIKeyIPWhitelist",[1408,2509,2510],{},"One row per allowed IP",[1390,2512,2513,2518,2523],{},[1408,2514,2515],{},[219,2516,2517],{},"api_key_usage_sync_batches",[1408,2519,2520],{},[219,2521,2522],{},"APIKeyUsageSyncBatch",[1408,2524,2525],{},"Redis -> Postgres usage-sync bookkeeping",[278,2527,2529],{"className":312,"code":2528,"language":314,"meta":286,"style":286},"class APIKeyStatus(str, Enum):\n    ACTIVE = \"active\"\n    SUSPENDED = \"suspended\"   # Temporarily disabled\n    REVOKED = \"revoked\"       # Permanently disabled\n    EXPIRED = \"expired\"       # Past expiration date\n\nclass APIKeyKind(str, Enum):\n    PERSONAL = \"personal\"\n    SYSTEM_INTEGRATION = \"system_integration\"\n\nclass APIKey(BaseModel, table=True):\n    __tablename__ = \"api_keys\"\n\n    # Key information\n    name: str\n    description: Optional[str] = None\n    prefix: str                      # Unique; e.g. \"sk_live_abc1\"\n    key_hash: str                    # Hash of the full key\n\n    # Ownership — exactly one of these two\n    owner_id: Optional[UUID] = None                   # FK -> users.id\n    integration_principal_id: Optional[UUID] = None   # FK -> integration_principals.id\n    key_kind: APIKeyKind = APIKeyKind.PERSONAL\n\n    # Status\n    status: APIKeyStatus = APIKeyStatus.ACTIVE\n    expires_at: Optional[datetime] = None\n    last_used_at: Optional[datetime] = None\n    usage_count: int = 0             # Synced from Redis\n\n    # Rate limiting\n    rate_limit_per_minute: int = 60\n    rate_limit_per_hour: Optional[int] = None\n    rate_limit_per_day: Optional[int] = None\n\n    # Entity scoping (EnterpriseRBAC)\n    entity_id: Optional[UUID] = None\n    inherit_from_tree: bool = ...\n",[219,2530,2531,2536,2541,2546,2551,2556,2560,2565,2570,2575,2579,2584,2589,2593,2598,2603,2607,2612,2617,2621,2626,2631,2636,2641,2645,2649,2654,2659,2663,2668,2672,2677,2682,2687,2692,2696,2701,2706],{"__ignoreMap":286},[318,2532,2533],{"class":320,"line":321},[318,2534,2535],{},"class APIKeyStatus(str, Enum):\n",[318,2537,2538],{"class":320,"line":327},[318,2539,2540],{},"    ACTIVE = \"active\"\n",[318,2542,2543],{"class":320,"line":333},[318,2544,2545],{},"    SUSPENDED = \"suspended\"   # Temporarily disabled\n",[318,2547,2548],{"class":320,"line":340},[318,2549,2550],{},"    REVOKED = \"revoked\"       # Permanently disabled\n",[318,2552,2553],{"class":320,"line":346},[318,2554,2555],{},"    EXPIRED = \"expired\"       # Past expiration date\n",[318,2557,2558],{"class":320,"line":352},[318,2559,337],{"emptyLinePlaceholder":336},[318,2561,2562],{"class":320,"line":358},[318,2563,2564],{},"class APIKeyKind(str, Enum):\n",[318,2566,2567],{"class":320,"line":364},[318,2568,2569],{},"    PERSONAL = \"personal\"\n",[318,2571,2572],{"class":320,"line":369},[318,2573,2574],{},"    SYSTEM_INTEGRATION = \"system_integration\"\n",[318,2576,2577],{"class":320,"line":374},[318,2578,337],{"emptyLinePlaceholder":336},[318,2580,2581],{"class":320,"line":380},[318,2582,2583],{},"class APIKey(BaseModel, table=True):\n",[318,2585,2586],{"class":320,"line":386},[318,2587,2588],{},"    __tablename__ = \"api_keys\"\n",[318,2590,2591],{"class":320,"line":392},[318,2592,337],{"emptyLinePlaceholder":336},[318,2594,2595],{"class":320,"line":398},[318,2596,2597],{},"    # Key information\n",[318,2599,2600],{"class":320,"line":404},[318,2601,2602],{},"    name: str\n",[318,2604,2605],{"class":320,"line":410},[318,2606,1519],{},[318,2608,2609],{"class":320,"line":416},[318,2610,2611],{},"    prefix: str                      # Unique; e.g. \"sk_live_abc1\"\n",[318,2613,2614],{"class":320,"line":422},[318,2615,2616],{},"    key_hash: str                    # Hash of the full key\n",[318,2618,2619],{"class":320,"line":428},[318,2620,337],{"emptyLinePlaceholder":336},[318,2622,2623],{"class":320,"line":434},[318,2624,2625],{},"    # Ownership — exactly one of these two\n",[318,2627,2628],{"class":320,"line":440},[318,2629,2630],{},"    owner_id: Optional[UUID] = None                   # FK -> users.id\n",[318,2632,2633],{"class":320,"line":445},[318,2634,2635],{},"    integration_principal_id: Optional[UUID] = None   # FK -> integration_principals.id\n",[318,2637,2638],{"class":320,"line":451},[318,2639,2640],{},"    key_kind: APIKeyKind = APIKeyKind.PERSONAL\n",[318,2642,2643],{"class":320,"line":456},[318,2644,337],{"emptyLinePlaceholder":336},[318,2646,2647],{"class":320,"line":461},[318,2648,730],{},[318,2650,2651],{"class":320,"line":466},[318,2652,2653],{},"    status: APIKeyStatus = APIKeyStatus.ACTIVE\n",[318,2655,2656],{"class":320,"line":472},[318,2657,2658],{},"    expires_at: Optional[datetime] = None\n",[318,2660,2661],{"class":320,"line":687},[318,2662,2261],{},[318,2664,2665],{"class":320,"line":692},[318,2666,2667],{},"    usage_count: int = 0             # Synced from Redis\n",[318,2669,2670],{"class":320,"line":698},[318,2671,337],{"emptyLinePlaceholder":336},[318,2673,2674],{"class":320,"line":704},[318,2675,2676],{},"    # Rate limiting\n",[318,2678,2679],{"class":320,"line":710},[318,2680,2681],{},"    rate_limit_per_minute: int = 60\n",[318,2683,2684],{"class":320,"line":716},[318,2685,2686],{},"    rate_limit_per_hour: Optional[int] = None\n",[318,2688,2689],{"class":320,"line":722},[318,2690,2691],{},"    rate_limit_per_day: Optional[int] = None\n",[318,2693,2694],{"class":320,"line":727},[318,2695,337],{"emptyLinePlaceholder":336},[318,2697,2698],{"class":320,"line":733},[318,2699,2700],{},"    # Entity scoping (EnterpriseRBAC)\n",[318,2702,2703],{"class":320,"line":739},[318,2704,2705],{},"    entity_id: Optional[UUID] = None\n",[318,2707,2708],{"class":320,"line":745},[318,2709,2710],{},"    inherit_from_tree: bool = ...\n",[198,2712,2713,503],{},[210,2714,910],{},[278,2716,2718],{"className":312,"code":2717,"language":314,"meta":286,"style":286},"UniqueConstraint(\"prefix\", name=\"uq_api_keys_prefix\")\nIndex(\"ix_api_keys_owner_id\", \"owner_id\")\nIndex(\"ix_api_keys_integration_principal_id\", \"integration_principal_id\")\nIndex(\"ix_api_keys_key_kind\", \"key_kind\")\nIndex(\"ix_api_keys_status_expires_at\", \"status\", \"expires_at\")  # cleanup\u002Fexpiry sweeps\n",[219,2719,2720,2725,2730,2735,2740],{"__ignoreMap":286},[318,2721,2722],{"class":320,"line":321},[318,2723,2724],{},"UniqueConstraint(\"prefix\", name=\"uq_api_keys_prefix\")\n",[318,2726,2727],{"class":320,"line":327},[318,2728,2729],{},"Index(\"ix_api_keys_owner_id\", \"owner_id\")\n",[318,2731,2732],{"class":320,"line":333},[318,2733,2734],{},"Index(\"ix_api_keys_integration_principal_id\", \"integration_principal_id\")\n",[318,2736,2737],{"class":320,"line":340},[318,2738,2739],{},"Index(\"ix_api_keys_key_kind\", \"key_kind\")\n",[318,2741,2742],{"class":320,"line":346},[318,2743,2744],{},"Index(\"ix_api_keys_status_expires_at\", \"status\", \"expires_at\")  # cleanup\u002Fexpiry sweeps\n",[198,2746,2747,503],{},[210,2748,973],{},[278,2750,2752],{"className":312,"code":2751,"language":314,"meta":286,"style":286},"@staticmethod\ndef generate_key(prefix_type: str = \"sk_live\") -> tuple[str, str]:\n    \"\"\"Generate new API key with prefix.\"\"\"\n\n@staticmethod\ndef hash_key(full_key: str) -> str:\n    \"\"\"Hash API key for storage.\"\"\"\n\n@staticmethod\ndef hash_key_bytes(full_key: str) -> bytes:\n    \"\"\"Hash API key for storage, as raw bytes.\"\"\"\n\ndef is_active(self) -> bool:\n    \"\"\"Check if key is active and not expired.\"\"\"\n\ndef record_usage(self) -> None:\n    \"\"\"Bump last_used_at and usage_count.\"\"\"\n\ndef revoke(self) -> None: ...\ndef suspend(self) -> None: ...\ndef reactivate(self) -> None: ...\n\n@property\ndef owner_type(self) -> str: ...\n\n@property\ndef resolved_owner_id(self) -> Optional[UUID]: ...\n",[219,2753,2754,2759,2764,2769,2773,2777,2782,2787,2791,2795,2800,2805,2809,2814,2819,2823,2827,2832,2836,2841,2846,2851,2855,2859,2864,2868,2872],{"__ignoreMap":286},[318,2755,2756],{"class":320,"line":321},[318,2757,2758],{},"@staticmethod\n",[318,2760,2761],{"class":320,"line":327},[318,2762,2763],{},"def generate_key(prefix_type: str = \"sk_live\") -> tuple[str, str]:\n",[318,2765,2766],{"class":320,"line":333},[318,2767,2768],{},"    \"\"\"Generate new API key with prefix.\"\"\"\n",[318,2770,2771],{"class":320,"line":340},[318,2772,337],{"emptyLinePlaceholder":336},[318,2774,2775],{"class":320,"line":346},[318,2776,2758],{},[318,2778,2779],{"class":320,"line":352},[318,2780,2781],{},"def hash_key(full_key: str) -> str:\n",[318,2783,2784],{"class":320,"line":358},[318,2785,2786],{},"    \"\"\"Hash API key for storage.\"\"\"\n",[318,2788,2789],{"class":320,"line":364},[318,2790,337],{"emptyLinePlaceholder":336},[318,2792,2793],{"class":320,"line":369},[318,2794,2758],{},[318,2796,2797],{"class":320,"line":374},[318,2798,2799],{},"def hash_key_bytes(full_key: str) -> bytes:\n",[318,2801,2802],{"class":320,"line":380},[318,2803,2804],{},"    \"\"\"Hash API key for storage, as raw bytes.\"\"\"\n",[318,2806,2807],{"class":320,"line":386},[318,2808,337],{"emptyLinePlaceholder":336},[318,2810,2811],{"class":320,"line":392},[318,2812,2813],{},"def is_active(self) -> bool:\n",[318,2815,2816],{"class":320,"line":398},[318,2817,2818],{},"    \"\"\"Check if key is active and not expired.\"\"\"\n",[318,2820,2821],{"class":320,"line":404},[318,2822,337],{"emptyLinePlaceholder":336},[318,2824,2825],{"class":320,"line":410},[318,2826,2389],{},[318,2828,2829],{"class":320,"line":416},[318,2830,2831],{},"    \"\"\"Bump last_used_at and usage_count.\"\"\"\n",[318,2833,2834],{"class":320,"line":422},[318,2835,337],{"emptyLinePlaceholder":336},[318,2837,2838],{"class":320,"line":428},[318,2839,2840],{},"def revoke(self) -> None: ...\n",[318,2842,2843],{"class":320,"line":434},[318,2844,2845],{},"def suspend(self) -> None: ...\n",[318,2847,2848],{"class":320,"line":440},[318,2849,2850],{},"def reactivate(self) -> None: ...\n",[318,2852,2853],{"class":320,"line":445},[318,2854,337],{"emptyLinePlaceholder":336},[318,2856,2857],{"class":320,"line":451},[318,2858,983],{},[318,2860,2861],{"class":320,"line":456},[318,2862,2863],{},"def owner_type(self) -> str: ...\n",[318,2865,2866],{"class":320,"line":461},[318,2867,337],{"emptyLinePlaceholder":336},[318,2869,2870],{"class":320,"line":466},[318,2871,983],{},[318,2873,2874],{"class":320,"line":472},[318,2875,2876],{},"def resolved_owner_id(self) -> Optional[UUID]: ...\n",[198,2878,2879,2880,2882,2883,2074,2885,2887],{},"Scope and IP checks are ",[210,2881,305],{}," methods on the model - they are resolved from\nthe ",[219,2884,2487],{},[219,2886,2502],{}," tables by the API key service.",[198,2889,2890,503],{},[210,2891,1256],{},[278,2893,2895],{"className":312,"code":2894,"language":314,"meta":286,"style":286},"# Generate key\nfull_key, prefix = APIKey.generate_key(\"sk_live\")\n# full_key: \"sk_live_abc123def456...\"\n# prefix: \"sk_live_abc1\"\n\n# Create API key\napi_key = APIKey(\n    name=\"Production API\",\n    prefix=prefix,\n    key_hash=APIKey.hash_key(full_key),\n    owner_id=user.id,\n    rate_limit_per_minute=100,\n)\nsession.add(api_key)\nawait session.flush()\n\n# Scopes are separate rows\nsession.add(APIKeyScope(api_key_id=api_key.id, scope=\"user:read\"))\nsession.add(APIKeyScope(api_key_id=api_key.id, scope=\"entity:read\"))\nawait session.commit()\n\n# Return full_key to user (only shown once!)\n",[219,2896,2897,2902,2907,2912,2917,2921,2926,2931,2936,2941,2946,2951,2956,2960,2965,2970,2974,2979,2984,2989,2993,2997],{"__ignoreMap":286},[318,2898,2899],{"class":320,"line":321},[318,2900,2901],{},"# Generate key\n",[318,2903,2904],{"class":320,"line":327},[318,2905,2906],{},"full_key, prefix = APIKey.generate_key(\"sk_live\")\n",[318,2908,2909],{"class":320,"line":333},[318,2910,2911],{},"# full_key: \"sk_live_abc123def456...\"\n",[318,2913,2914],{"class":320,"line":340},[318,2915,2916],{},"# prefix: \"sk_live_abc1\"\n",[318,2918,2919],{"class":320,"line":346},[318,2920,337],{"emptyLinePlaceholder":336},[318,2922,2923],{"class":320,"line":352},[318,2924,2925],{},"# Create API key\n",[318,2927,2928],{"class":320,"line":358},[318,2929,2930],{},"api_key = APIKey(\n",[318,2932,2933],{"class":320,"line":364},[318,2934,2935],{},"    name=\"Production API\",\n",[318,2937,2938],{"class":320,"line":369},[318,2939,2940],{},"    prefix=prefix,\n",[318,2942,2943],{"class":320,"line":374},[318,2944,2945],{},"    key_hash=APIKey.hash_key(full_key),\n",[318,2947,2948],{"class":320,"line":380},[318,2949,2950],{},"    owner_id=user.id,\n",[318,2952,2953],{"class":320,"line":386},[318,2954,2955],{},"    rate_limit_per_minute=100,\n",[318,2957,2958],{"class":320,"line":392},[318,2959,1306],{},[318,2961,2962],{"class":320,"line":398},[318,2963,2964],{},"session.add(api_key)\n",[318,2966,2967],{"class":320,"line":404},[318,2968,2969],{},"await session.flush()\n",[318,2971,2972],{"class":320,"line":410},[318,2973,337],{"emptyLinePlaceholder":336},[318,2975,2976],{"class":320,"line":416},[318,2977,2978],{},"# Scopes are separate rows\n",[318,2980,2981],{"class":320,"line":422},[318,2982,2983],{},"session.add(APIKeyScope(api_key_id=api_key.id, scope=\"user:read\"))\n",[318,2985,2986],{"class":320,"line":428},[318,2987,2988],{},"session.add(APIKeyScope(api_key_id=api_key.id, scope=\"entity:read\"))\n",[318,2990,2991],{"class":320,"line":434},[318,2992,1316],{},[318,2994,2995],{"class":320,"line":440},[318,2996,337],{"emptyLinePlaceholder":336},[318,2998,2999],{"class":320,"line":445},[318,3000,3001],{},"# Return full_key to user (only shown once!)\n",[198,3003,3004,3007],{},[210,3005,3006],{},"See",": [[23-API-Keys]] for complete guide.",[271,3009],{},[539,3011,3013],{"id":3012},"_6-socialaccount","6. SocialAccount",[198,3015,3016],{},[210,3017,3018],{},"OAuth\u002Fsocial login accounts.",[278,3020,3022],{"className":312,"code":3021,"language":314,"meta":286,"style":286},"class SocialAccount(BaseModel, table=True):\n    __tablename__ = \"social_accounts\"\n\n    # User relationship\n    user_id: UUID                    # FK -> users.id\n\n    # Provider info\n    provider: str                    # \"google\", \"github\", \"facebook\", \"apple\"\n    provider_user_id: str\n    provider_email: Optional[str] = None\n    provider_email_verified: bool = False\n    provider_username: Optional[str] = None\n\n    # OAuth tokens (stored only when store_oauth_provider_tokens is enabled)\n    access_token: Optional[str] = None\n    refresh_token: Optional[str] = None\n    token_expires_at: Optional[datetime] = None\n\n    # Cached profile\n    display_name: Optional[str] = None\n    avatar_url: Optional[str] = None\n    profile_url: Optional[str] = None\n\n    # Metadata\n    last_login_at: Optional[datetime] = None\n    token_refreshed_at: Optional[datetime] = None\n\n    # Relationship\n    user: \"User\"\n",[219,3023,3024,3029,3034,3038,3043,3048,3052,3057,3062,3067,3072,3077,3082,3086,3091,3096,3101,3106,3110,3115,3120,3124,3129,3133,3138,3143,3148,3152,3156],{"__ignoreMap":286},[318,3025,3026],{"class":320,"line":321},[318,3027,3028],{},"class SocialAccount(BaseModel, table=True):\n",[318,3030,3031],{"class":320,"line":327},[318,3032,3033],{},"    __tablename__ = \"social_accounts\"\n",[318,3035,3036],{"class":320,"line":333},[318,3037,337],{"emptyLinePlaceholder":336},[318,3039,3040],{"class":320,"line":340},[318,3041,3042],{},"    # User relationship\n",[318,3044,3045],{"class":320,"line":346},[318,3046,3047],{},"    user_id: UUID                    # FK -> users.id\n",[318,3049,3050],{"class":320,"line":352},[318,3051,337],{"emptyLinePlaceholder":336},[318,3053,3054],{"class":320,"line":358},[318,3055,3056],{},"    # Provider info\n",[318,3058,3059],{"class":320,"line":364},[318,3060,3061],{},"    provider: str                    # \"google\", \"github\", \"facebook\", \"apple\"\n",[318,3063,3064],{"class":320,"line":369},[318,3065,3066],{},"    provider_user_id: str\n",[318,3068,3069],{"class":320,"line":374},[318,3070,3071],{},"    provider_email: Optional[str] = None\n",[318,3073,3074],{"class":320,"line":380},[318,3075,3076],{},"    provider_email_verified: bool = False\n",[318,3078,3079],{"class":320,"line":386},[318,3080,3081],{},"    provider_username: Optional[str] = None\n",[318,3083,3084],{"class":320,"line":392},[318,3085,337],{"emptyLinePlaceholder":336},[318,3087,3088],{"class":320,"line":398},[318,3089,3090],{},"    # OAuth tokens (stored only when store_oauth_provider_tokens is enabled)\n",[318,3092,3093],{"class":320,"line":404},[318,3094,3095],{},"    access_token: Optional[str] = None\n",[318,3097,3098],{"class":320,"line":410},[318,3099,3100],{},"    refresh_token: Optional[str] = None\n",[318,3102,3103],{"class":320,"line":416},[318,3104,3105],{},"    token_expires_at: Optional[datetime] = None\n",[318,3107,3108],{"class":320,"line":422},[318,3109,337],{"emptyLinePlaceholder":336},[318,3111,3112],{"class":320,"line":428},[318,3113,3114],{},"    # Cached profile\n",[318,3116,3117],{"class":320,"line":434},[318,3118,3119],{},"    display_name: Optional[str] = None\n",[318,3121,3122],{"class":320,"line":440},[318,3123,701],{},[318,3125,3126],{"class":320,"line":445},[318,3127,3128],{},"    profile_url: Optional[str] = None\n",[318,3130,3131],{"class":320,"line":451},[318,3132,337],{"emptyLinePlaceholder":336},[318,3134,3135],{"class":320,"line":456},[318,3136,3137],{},"    # Metadata\n",[318,3139,3140],{"class":320,"line":461},[318,3141,3142],{},"    last_login_at: Optional[datetime] = None\n",[318,3144,3145],{"class":320,"line":466},[318,3146,3147],{},"    token_refreshed_at: Optional[datetime] = None\n",[318,3149,3150],{"class":320,"line":472},[318,3151,337],{"emptyLinePlaceholder":336},[318,3153,3154],{"class":320,"line":687},[318,3155,2275],{},[318,3157,3158],{"class":320,"line":692},[318,3159,2280],{},[198,3161,3162,503],{},[210,3163,910],{},[278,3165,3167],{"className":312,"code":3166,"language":314,"meta":286,"style":286},"UniqueConstraint(\"provider\", \"provider_user_id\", name=\"uq_social_provider_user\")\nIndex(\"ix_social_accounts_user_id\", \"user_id\")\nIndex(\"ix_social_accounts_provider\", \"provider\")\n",[219,3168,3169,3174,3179],{"__ignoreMap":286},[318,3170,3171],{"class":320,"line":321},[318,3172,3173],{},"UniqueConstraint(\"provider\", \"provider_user_id\", name=\"uq_social_provider_user\")\n",[318,3175,3176],{"class":320,"line":327},[318,3177,3178],{},"Index(\"ix_social_accounts_user_id\", \"user_id\")\n",[318,3180,3181],{"class":320,"line":333},[318,3182,3183],{},"Index(\"ix_social_accounts_provider\", \"provider\")\n",[198,3185,3186,503],{},[210,3187,1256],{},[278,3189,3191],{"className":312,"code":3190,"language":314,"meta":286,"style":286},"social = SocialAccount(\n    user_id=user.id,\n    provider=\"google\",\n    provider_user_id=\"107353926327...\",\n    provider_email=\"john@gmail.com\",\n    provider_email_verified=True,\n    display_name=\"John Doe\",\n    avatar_url=\"https:\u002F\u002Flh3.googleusercontent.com\u002F...\",\n)\nsession.add(social)\nawait session.commit()\n",[219,3192,3193,3198,3202,3207,3212,3217,3222,3227,3232,3236,3241],{"__ignoreMap":286},[318,3194,3195],{"class":320,"line":321},[318,3196,3197],{},"social = SocialAccount(\n",[318,3199,3200],{"class":320,"line":327},[318,3201,2413],{},[318,3203,3204],{"class":320,"line":333},[318,3205,3206],{},"    provider=\"google\",\n",[318,3208,3209],{"class":320,"line":340},[318,3210,3211],{},"    provider_user_id=\"107353926327...\",\n",[318,3213,3214],{"class":320,"line":346},[318,3215,3216],{},"    provider_email=\"john@gmail.com\",\n",[318,3218,3219],{"class":320,"line":352},[318,3220,3221],{},"    provider_email_verified=True,\n",[318,3223,3224],{"class":320,"line":358},[318,3225,3226],{},"    display_name=\"John Doe\",\n",[318,3228,3229],{"class":320,"line":364},[318,3230,3231],{},"    avatar_url=\"https:\u002F\u002Flh3.googleusercontent.com\u002F...\",\n",[318,3233,3234],{"class":320,"line":369},[318,3235,1306],{},[318,3237,3238],{"class":320,"line":374},[318,3239,3240],{},"session.add(social)\n",[318,3242,3243],{"class":320,"line":380},[318,3244,1316],{},[198,3246,3247,3249],{},[210,3248,3006],{},": [[33-OAuth-Account-Linking]] for account linking.",[271,3251],{},[202,3253,3255],{"id":3254},"enterpriserbac-models","EnterpriseRBAC Models",[539,3257,3259],{"id":3258},"_7-entity","7. Entity",[198,3261,3262,548,3265],{},[210,3263,3264],{},"Organizational entities and access groups.",[219,3266,3267],{},"entities",[278,3269,3271],{"className":312,"code":3270,"language":314,"meta":286,"style":286},"class EntityClass(str, Enum):\n    STRUCTURAL = \"structural\"      # Organizational units (company, department, team)\n    ACCESS_GROUP = \"access_group\"  # Permission groupings (project, resource pool)\n\nclass Entity(BaseModel, table=True):\n    __tablename__ = \"entities\"\n\n    # Identity\n    name: str                        # System name\n    display_name: str                # User-friendly name\n    slug: str                        # URL-friendly, unique\n    description: Optional[str] = None\n\n    # Classification\n    entity_class: EntityClass\n    entity_type: str                 # \"company\", \"department\", \"team\", etc.\n\n    # Hierarchy\n    parent_id: Optional[UUID] = None  # FK -> entities.id\n    depth: int = ...                  # Materialized depth in the tree\n    path: Optional[str] = None        # Materialized path\n\n    # Lifecycle\n    status: str = \"active\"           # active, inactive, archived\n    valid_from: Optional[datetime] = None\n    valid_until: Optional[datetime] = None\n\n    # Configuration (per-entity child type customization - DD-051)\n    max_members: Optional[int] = None\n    max_depth: Optional[int] = None\n    allowed_child_types: List[str] = []\n    # If empty, uses system default child types from SystemConfig\n\n    allowed_child_classes: List[str] = []\n    # Entity classes allowed as children (e.g., [\"structural\", \"access_group\"])\n\n    # Child naming guidance\n    child_name_pattern: Optional[str] = None\n    child_display_name_pattern: Optional[str] = None\n    child_slug_pattern: Optional[str] = None\n    child_naming_guidance: Optional[str] = None\n\n    # Relationships\n    parent: Optional[\"Entity\"]\n    children: List[\"Entity\"]\n    memberships: List[\"EntityMembership\"]\n    scoped_roles: List[\"Role\"]\n    users: List[\"User\"]\n",[219,3272,3273,3278,3283,3288,3292,3297,3302,3306,3310,3315,3320,3325,3329,3333,3338,3343,3348,3352,3357,3362,3367,3372,3376,3381,3386,3391,3396,3400,3405,3410,3415,3420,3425,3429,3434,3439,3443,3448,3453,3458,3463,3468,3472,3476,3481,3486,3491,3496],{"__ignoreMap":286},[318,3274,3275],{"class":320,"line":321},[318,3276,3277],{},"class EntityClass(str, Enum):\n",[318,3279,3280],{"class":320,"line":327},[318,3281,3282],{},"    STRUCTURAL = \"structural\"      # Organizational units (company, department, team)\n",[318,3284,3285],{"class":320,"line":333},[318,3286,3287],{},"    ACCESS_GROUP = \"access_group\"  # Permission groupings (project, resource pool)\n",[318,3289,3290],{"class":320,"line":340},[318,3291,337],{"emptyLinePlaceholder":336},[318,3293,3294],{"class":320,"line":346},[318,3295,3296],{},"class Entity(BaseModel, table=True):\n",[318,3298,3299],{"class":320,"line":352},[318,3300,3301],{},"    __tablename__ = \"entities\"\n",[318,3303,3304],{"class":320,"line":358},[318,3305,337],{"emptyLinePlaceholder":336},[318,3307,3308],{"class":320,"line":364},[318,3309,1504],{},[318,3311,3312],{"class":320,"line":369},[318,3313,3314],{},"    name: str                        # System name\n",[318,3316,3317],{"class":320,"line":374},[318,3318,3319],{},"    display_name: str                # User-friendly name\n",[318,3321,3322],{"class":320,"line":380},[318,3323,3324],{},"    slug: str                        # URL-friendly, unique\n",[318,3326,3327],{"class":320,"line":386},[318,3328,1519],{},[318,3330,3331],{"class":320,"line":392},[318,3332,337],{"emptyLinePlaceholder":336},[318,3334,3335],{"class":320,"line":398},[318,3336,3337],{},"    # Classification\n",[318,3339,3340],{"class":320,"line":404},[318,3341,3342],{},"    entity_class: EntityClass\n",[318,3344,3345],{"class":320,"line":410},[318,3346,3347],{},"    entity_type: str                 # \"company\", \"department\", \"team\", etc.\n",[318,3349,3350],{"class":320,"line":416},[318,3351,337],{"emptyLinePlaceholder":336},[318,3353,3354],{"class":320,"line":422},[318,3355,3356],{},"    # Hierarchy\n",[318,3358,3359],{"class":320,"line":428},[318,3360,3361],{},"    parent_id: Optional[UUID] = None  # FK -> entities.id\n",[318,3363,3364],{"class":320,"line":434},[318,3365,3366],{},"    depth: int = ...                  # Materialized depth in the tree\n",[318,3368,3369],{"class":320,"line":440},[318,3370,3371],{},"    path: Optional[str] = None        # Materialized path\n",[318,3373,3374],{"class":320,"line":445},[318,3375,337],{"emptyLinePlaceholder":336},[318,3377,3378],{"class":320,"line":451},[318,3379,3380],{},"    # Lifecycle\n",[318,3382,3383],{"class":320,"line":456},[318,3384,3385],{},"    status: str = \"active\"           # active, inactive, archived\n",[318,3387,3388],{"class":320,"line":461},[318,3389,3390],{},"    valid_from: Optional[datetime] = None\n",[318,3392,3393],{"class":320,"line":466},[318,3394,3395],{},"    valid_until: Optional[datetime] = None\n",[318,3397,3398],{"class":320,"line":472},[318,3399,337],{"emptyLinePlaceholder":336},[318,3401,3402],{"class":320,"line":687},[318,3403,3404],{},"    # Configuration (per-entity child type customization - DD-051)\n",[318,3406,3407],{"class":320,"line":692},[318,3408,3409],{},"    max_members: Optional[int] = None\n",[318,3411,3412],{"class":320,"line":698},[318,3413,3414],{},"    max_depth: Optional[int] = None\n",[318,3416,3417],{"class":320,"line":704},[318,3418,3419],{},"    allowed_child_types: List[str] = []\n",[318,3421,3422],{"class":320,"line":710},[318,3423,3424],{},"    # If empty, uses system default child types from SystemConfig\n",[318,3426,3427],{"class":320,"line":716},[318,3428,337],{"emptyLinePlaceholder":336},[318,3430,3431],{"class":320,"line":722},[318,3432,3433],{},"    allowed_child_classes: List[str] = []\n",[318,3435,3436],{"class":320,"line":727},[318,3437,3438],{},"    # Entity classes allowed as children (e.g., [\"structural\", \"access_group\"])\n",[318,3440,3441],{"class":320,"line":733},[318,3442,337],{"emptyLinePlaceholder":336},[318,3444,3445],{"class":320,"line":739},[318,3446,3447],{},"    # Child naming guidance\n",[318,3449,3450],{"class":320,"line":745},[318,3451,3452],{},"    child_name_pattern: Optional[str] = None\n",[318,3454,3455],{"class":320,"line":751},[318,3456,3457],{},"    child_display_name_pattern: Optional[str] = None\n",[318,3459,3460],{"class":320,"line":757},[318,3461,3462],{},"    child_slug_pattern: Optional[str] = None\n",[318,3464,3465],{"class":320,"line":763},[318,3466,3467],{},"    child_naming_guidance: Optional[str] = None\n",[318,3469,3470],{"class":320,"line":769},[318,3471,337],{"emptyLinePlaceholder":336},[318,3473,3474],{"class":320,"line":774},[318,3475,1600],{},[318,3477,3478],{"class":320,"line":780},[318,3479,3480],{},"    parent: Optional[\"Entity\"]\n",[318,3482,3483],{"class":320,"line":786},[318,3484,3485],{},"    children: List[\"Entity\"]\n",[318,3487,3488],{"class":320,"line":792},[318,3489,3490],{},"    memberships: List[\"EntityMembership\"]\n",[318,3492,3493],{"class":320,"line":798},[318,3494,3495],{},"    scoped_roles: List[\"Role\"]\n",[318,3497,3498],{"class":320,"line":804},[318,3499,3500],{},"    users: List[\"User\"]\n",[198,3502,3503,3504,3507,3508,3511,3512,3515,3516,3519],{},"Note the hierarchy link is a plain ",[219,3505,3506],{},"parent_id"," FK plus materialized ",[219,3509,3510],{},"depth"," \u002F\n",[219,3513,3514],{},"path"," columns; ancestor-descendant queries go through the ",[219,3517,3518],{},"entity_closure","\ntable (section 9).",[198,3521,3522,3525],{},[219,3523,3524],{},"Entity.metadata"," is reserved for a future persisted feature. It is not part of the live SQL entity model or API contract today.",[198,3527,3528,3531],{},[210,3529,3530],{},"Child Type Configuration (DD-051)",":\nRoot entities can define their own allowed child types, which override the system defaults:",[278,3533,3535],{"className":312,"code":3534,"language":314,"meta":286,"style":286},"# Root entity with custom child types\nbrokerage = Entity(\n    name=\"acme_realty\",\n    display_name=\"ACME Realty\",\n    entity_type=\"brokerage\",\n    parent_id=None,  # Root entity\n    allowed_child_types=[\"branch\", \"team\", \"agent\"],  # Custom for this brokerage\n)\n\n# Child entities under this root can only use these types\nbranch = Entity(\n    name=\"downtown_branch\",\n    parent_id=brokerage.id,\n    entity_type=\"branch\",  # Must be in parent's allowed_child_types\n)\n",[219,3536,3537,3542,3547,3552,3557,3562,3567,3572,3576,3580,3585,3590,3595,3600,3605],{"__ignoreMap":286},[318,3538,3539],{"class":320,"line":321},[318,3540,3541],{},"# Root entity with custom child types\n",[318,3543,3544],{"class":320,"line":327},[318,3545,3546],{},"brokerage = Entity(\n",[318,3548,3549],{"class":320,"line":333},[318,3550,3551],{},"    name=\"acme_realty\",\n",[318,3553,3554],{"class":320,"line":340},[318,3555,3556],{},"    display_name=\"ACME Realty\",\n",[318,3558,3559],{"class":320,"line":346},[318,3560,3561],{},"    entity_type=\"brokerage\",\n",[318,3563,3564],{"class":320,"line":352},[318,3565,3566],{},"    parent_id=None,  # Root entity\n",[318,3568,3569],{"class":320,"line":358},[318,3570,3571],{},"    allowed_child_types=[\"branch\", \"team\", \"agent\"],  # Custom for this brokerage\n",[318,3573,3574],{"class":320,"line":364},[318,3575,1306],{},[318,3577,3578],{"class":320,"line":369},[318,3579,337],{"emptyLinePlaceholder":336},[318,3581,3582],{"class":320,"line":374},[318,3583,3584],{},"# Child entities under this root can only use these types\n",[318,3586,3587],{"class":320,"line":380},[318,3588,3589],{},"branch = Entity(\n",[318,3591,3592],{"class":320,"line":386},[318,3593,3594],{},"    name=\"downtown_branch\",\n",[318,3596,3597],{"class":320,"line":392},[318,3598,3599],{},"    parent_id=brokerage.id,\n",[318,3601,3602],{"class":320,"line":398},[318,3603,3604],{},"    entity_type=\"branch\",  # Must be in parent's allowed_child_types\n",[318,3606,3607],{"class":320,"line":404},[318,3608,1306],{},[198,3610,3611,3612,3615],{},"If ",[219,3613,3614],{},"allowed_child_types"," is empty on the root entity, the system defaults from SystemConfig are used.",[198,3617,3618,503],{},[210,3619,910],{},[278,3621,3623],{"className":312,"code":3622,"language":314,"meta":286,"style":286},"UniqueConstraint(\"slug\", name=\"uq_entities_slug\")\nIndex(\"ix_entities_name\", \"name\")\nIndex(\"ix_entities_class_type\", \"entity_class\", \"entity_type\")\nIndex(\"ix_entities_parent_id\", \"parent_id\")\nIndex(\"ix_entities_status\", \"status\")\n",[219,3624,3625,3630,3635,3640,3645],{"__ignoreMap":286},[318,3626,3627],{"class":320,"line":321},[318,3628,3629],{},"UniqueConstraint(\"slug\", name=\"uq_entities_slug\")\n",[318,3631,3632],{"class":320,"line":327},[318,3633,3634],{},"Index(\"ix_entities_name\", \"name\")\n",[318,3636,3637],{"class":320,"line":333},[318,3638,3639],{},"Index(\"ix_entities_class_type\", \"entity_class\", \"entity_type\")\n",[318,3641,3642],{"class":320,"line":340},[318,3643,3644],{},"Index(\"ix_entities_parent_id\", \"parent_id\")\n",[318,3646,3647],{"class":320,"line":346},[318,3648,3649],{},"Index(\"ix_entities_status\", \"status\")\n",[198,3651,3652,503],{},[210,3653,973],{},[278,3655,3657],{"className":312,"code":3656,"language":314,"meta":286,"style":286},"@property\ndef is_structural(self) -> bool:\n    \"\"\"Check if entity is structural.\"\"\"\n\n@property\ndef is_access_group(self) -> bool:\n    \"\"\"Check if entity is an access group.\"\"\"\n\n@property\ndef is_root(self) -> bool:\n    \"\"\"Check if entity has no parent.\"\"\"\n\ndef is_active(self) -> bool:\n    \"\"\"Check if entity is currently active.\"\"\"\n\ndef update_path(self, parent_path: Optional[str] = None) -> None:\n    \"\"\"Recompute the materialized path from the parent's path.\"\"\"\n",[219,3658,3659,3663,3668,3673,3677,3681,3686,3691,3695,3699,3704,3709,3713,3717,3722,3726,3731],{"__ignoreMap":286},[318,3660,3661],{"class":320,"line":321},[318,3662,983],{},[318,3664,3665],{"class":320,"line":327},[318,3666,3667],{},"def is_structural(self) -> bool:\n",[318,3669,3670],{"class":320,"line":333},[318,3671,3672],{},"    \"\"\"Check if entity is structural.\"\"\"\n",[318,3674,3675],{"class":320,"line":340},[318,3676,337],{"emptyLinePlaceholder":336},[318,3678,3679],{"class":320,"line":346},[318,3680,983],{},[318,3682,3683],{"class":320,"line":352},[318,3684,3685],{},"def is_access_group(self) -> bool:\n",[318,3687,3688],{"class":320,"line":358},[318,3689,3690],{},"    \"\"\"Check if entity is an access group.\"\"\"\n",[318,3692,3693],{"class":320,"line":364},[318,3694,337],{"emptyLinePlaceholder":336},[318,3696,3697],{"class":320,"line":369},[318,3698,983],{},[318,3700,3701],{"class":320,"line":374},[318,3702,3703],{},"def is_root(self) -> bool:\n",[318,3705,3706],{"class":320,"line":380},[318,3707,3708],{},"    \"\"\"Check if entity has no parent.\"\"\"\n",[318,3710,3711],{"class":320,"line":386},[318,3712,337],{"emptyLinePlaceholder":336},[318,3714,3715],{"class":320,"line":392},[318,3716,2813],{},[318,3718,3719],{"class":320,"line":398},[318,3720,3721],{},"    \"\"\"Check if entity is currently active.\"\"\"\n",[318,3723,3724],{"class":320,"line":404},[318,3725,337],{"emptyLinePlaceholder":336},[318,3727,3728],{"class":320,"line":410},[318,3729,3730],{},"def update_path(self, parent_path: Optional[str] = None) -> None:\n",[318,3732,3733],{"class":320,"line":416},[318,3734,3735],{},"    \"\"\"Recompute the materialized path from the parent's path.\"\"\"\n",[198,3737,3738,3739,3742,3743,3745],{},"Ancestor checks are not a method on ",[219,3740,3741],{},"Entity"," - query ",[219,3744,3518],{}," instead\n(section 9).",[198,3747,3748,503],{},[210,3749,1256],{},[278,3751,3753],{"className":312,"code":3752,"language":314,"meta":286,"style":286},"company = Entity(\n    name=\"acme_corp\",\n    display_name=\"Acme Corp\",\n    slug=\"acme-corp\",\n    entity_class=EntityClass.STRUCTURAL,\n    entity_type=\"company\",\n    status=\"active\",\n)\nsession.add(company)\nawait session.flush()\n\nengineering = Entity(\n    name=\"engineering\",\n    display_name=\"Engineering\",\n    slug=\"acme-corp-engineering\",\n    entity_class=EntityClass.STRUCTURAL,\n    entity_type=\"department\",\n    parent_id=company.id,\n)\nsession.add(engineering)\nawait session.commit()\n",[219,3754,3755,3760,3765,3770,3775,3780,3785,3790,3794,3799,3803,3807,3812,3817,3822,3827,3831,3836,3841,3845,3850],{"__ignoreMap":286},[318,3756,3757],{"class":320,"line":321},[318,3758,3759],{},"company = Entity(\n",[318,3761,3762],{"class":320,"line":327},[318,3763,3764],{},"    name=\"acme_corp\",\n",[318,3766,3767],{"class":320,"line":333},[318,3768,3769],{},"    display_name=\"Acme Corp\",\n",[318,3771,3772],{"class":320,"line":340},[318,3773,3774],{},"    slug=\"acme-corp\",\n",[318,3776,3777],{"class":320,"line":346},[318,3778,3779],{},"    entity_class=EntityClass.STRUCTURAL,\n",[318,3781,3782],{"class":320,"line":352},[318,3783,3784],{},"    entity_type=\"company\",\n",[318,3786,3787],{"class":320,"line":358},[318,3788,3789],{},"    status=\"active\",\n",[318,3791,3792],{"class":320,"line":364},[318,3793,1306],{},[318,3795,3796],{"class":320,"line":369},[318,3797,3798],{},"session.add(company)\n",[318,3800,3801],{"class":320,"line":374},[318,3802,2969],{},[318,3804,3805],{"class":320,"line":380},[318,3806,337],{"emptyLinePlaceholder":336},[318,3808,3809],{"class":320,"line":386},[318,3810,3811],{},"engineering = Entity(\n",[318,3813,3814],{"class":320,"line":392},[318,3815,3816],{},"    name=\"engineering\",\n",[318,3818,3819],{"class":320,"line":398},[318,3820,3821],{},"    display_name=\"Engineering\",\n",[318,3823,3824],{"class":320,"line":404},[318,3825,3826],{},"    slug=\"acme-corp-engineering\",\n",[318,3828,3829],{"class":320,"line":410},[318,3830,3779],{},[318,3832,3833],{"class":320,"line":416},[318,3834,3835],{},"    entity_type=\"department\",\n",[318,3837,3838],{"class":320,"line":422},[318,3839,3840],{},"    parent_id=company.id,\n",[318,3842,3843],{"class":320,"line":428},[318,3844,1306],{},[318,3846,3847],{"class":320,"line":434},[318,3848,3849],{},"session.add(engineering)\n",[318,3851,3852],{"class":320,"line":440},[318,3853,1316],{},[198,3855,3856,3858],{},[210,3857,3006],{},": [[50-Entity-System]] for entity system overview.",[271,3860],{},[539,3862,3864],{"id":3863},"_8-entitymembership","8. EntityMembership",[198,3866,3867],{},[210,3868,3869],{},"User memberships in entities with roles.",[198,3871,3872,3873,3876,3877,3880],{},"Roles are attached through the ",[219,3874,3875],{},"entity_membership_roles"," junction table\n(",[219,3878,3879],{},"EntityMembershipRole","), not an embedded list.",[278,3882,3884],{"className":312,"code":3883,"language":314,"meta":286,"style":286},"from outlabs_auth.models.sql.enums import MembershipStatus\n\nclass EntityMembership(BaseModel, table=True):\n    __tablename__ = \"entity_memberships\"\n\n    # Relationships\n    user_id: UUID                    # FK -> users.id\n    entity_id: UUID                  # FK -> entities.id\n\n    # Membership metadata\n    joined_at: datetime\n    joined_by_id: Optional[UUID] = None   # FK -> users.id\n\n    # Time-based membership\n    valid_from: Optional[datetime] = None\n    valid_until: Optional[datetime] = None\n\n    # Status\n    status: MembershipStatus = MembershipStatus.ACTIVE\n\n    # Revocation metadata (when status=REVOKED)\n    revoked_at: Optional[datetime] = None\n    revoked_by_id: Optional[UUID] = None  # FK -> users.id\n    revocation_reason: Optional[str] = None\n\n    # Relationships\n    user: \"User\"\n    entity: \"Entity\"\n    roles: List[\"Role\"]              # via entity_membership_roles\n",[219,3885,3886,3891,3895,3900,3905,3909,3913,3917,3922,3926,3931,3936,3941,3945,3950,3954,3958,3962,3966,3971,3975,3980,3984,3989,3994,3998,4002,4006,4011],{"__ignoreMap":286},[318,3887,3888],{"class":320,"line":321},[318,3889,3890],{},"from outlabs_auth.models.sql.enums import MembershipStatus\n",[318,3892,3893],{"class":320,"line":327},[318,3894,337],{"emptyLinePlaceholder":336},[318,3896,3897],{"class":320,"line":333},[318,3898,3899],{},"class EntityMembership(BaseModel, table=True):\n",[318,3901,3902],{"class":320,"line":340},[318,3903,3904],{},"    __tablename__ = \"entity_memberships\"\n",[318,3906,3907],{"class":320,"line":346},[318,3908,337],{"emptyLinePlaceholder":336},[318,3910,3911],{"class":320,"line":352},[318,3912,1600],{},[318,3914,3915],{"class":320,"line":358},[318,3916,3047],{},[318,3918,3919],{"class":320,"line":364},[318,3920,3921],{},"    entity_id: UUID                  # FK -> entities.id\n",[318,3923,3924],{"class":320,"line":369},[318,3925,337],{"emptyLinePlaceholder":336},[318,3927,3928],{"class":320,"line":374},[318,3929,3930],{},"    # Membership metadata\n",[318,3932,3933],{"class":320,"line":380},[318,3934,3935],{},"    joined_at: datetime\n",[318,3937,3938],{"class":320,"line":386},[318,3939,3940],{},"    joined_by_id: Optional[UUID] = None   # FK -> users.id\n",[318,3942,3943],{"class":320,"line":392},[318,3944,337],{"emptyLinePlaceholder":336},[318,3946,3947],{"class":320,"line":398},[318,3948,3949],{},"    # Time-based membership\n",[318,3951,3952],{"class":320,"line":404},[318,3953,3390],{},[318,3955,3956],{"class":320,"line":410},[318,3957,3395],{},[318,3959,3960],{"class":320,"line":416},[318,3961,337],{"emptyLinePlaceholder":336},[318,3963,3964],{"class":320,"line":422},[318,3965,730],{},[318,3967,3968],{"class":320,"line":428},[318,3969,3970],{},"    status: MembershipStatus = MembershipStatus.ACTIVE\n",[318,3972,3973],{"class":320,"line":434},[318,3974,337],{"emptyLinePlaceholder":336},[318,3976,3977],{"class":320,"line":440},[318,3978,3979],{},"    # Revocation metadata (when status=REVOKED)\n",[318,3981,3982],{"class":320,"line":445},[318,3983,2213],{},[318,3985,3986],{"class":320,"line":451},[318,3987,3988],{},"    revoked_by_id: Optional[UUID] = None  # FK -> users.id\n",[318,3990,3991],{"class":320,"line":456},[318,3992,3993],{},"    revocation_reason: Optional[str] = None\n",[318,3995,3996],{"class":320,"line":461},[318,3997,337],{"emptyLinePlaceholder":336},[318,3999,4000],{"class":320,"line":466},[318,4001,1600],{},[318,4003,4004],{"class":320,"line":472},[318,4005,2280],{},[318,4007,4008],{"class":320,"line":687},[318,4009,4010],{},"    entity: \"Entity\"\n",[318,4012,4013],{"class":320,"line":692},[318,4014,4015],{},"    roles: List[\"Role\"]              # via entity_membership_roles\n",[198,4017,4018,503],{},[210,4019,910],{},[278,4021,4023],{"className":312,"code":4022,"language":314,"meta":286,"style":286},"UniqueConstraint(\"user_id\", \"entity_id\", name=\"uq_entity_membership\")\nIndex(\"ix_em_entity_id\", \"entity_id\")\nIndex(\"ix_em_user_status\", \"user_id\", \"status\")\nIndex(\"ix_em_status\", \"status\")\nIndex(\"ix_em_valid_until\", \"valid_until\")\n",[219,4024,4025,4030,4035,4040,4045],{"__ignoreMap":286},[318,4026,4027],{"class":320,"line":321},[318,4028,4029],{},"UniqueConstraint(\"user_id\", \"entity_id\", name=\"uq_entity_membership\")\n",[318,4031,4032],{"class":320,"line":327},[318,4033,4034],{},"Index(\"ix_em_entity_id\", \"entity_id\")\n",[318,4036,4037],{"class":320,"line":333},[318,4038,4039],{},"Index(\"ix_em_user_status\", \"user_id\", \"status\")\n",[318,4041,4042],{"class":320,"line":340},[318,4043,4044],{},"Index(\"ix_em_status\", \"status\")\n",[318,4046,4047],{"class":320,"line":346},[318,4048,4049],{},"Index(\"ix_em_valid_until\", \"valid_until\")\n",[198,4051,4052,4053,4056,4057,269],{},"Membership changes are also recorded in ",[219,4054,4055],{},"entity_membership_history","\n(",[219,4058,4059],{},"EntityMembershipHistory",[198,4061,4062,503],{},[210,4063,973],{},[278,4065,4067],{"className":312,"code":4066,"language":314,"meta":286,"style":286},"def is_currently_valid(self) -> bool:\n    \"\"\"Check if membership is currently valid (time-based).\"\"\"\n\ndef can_grant_permissions(self) -> bool:\n    \"\"\"Check if membership can currently grant permissions (status + time).\"\"\"\n\ndef get_role_names(self) -> List[str]:\n    \"\"\"Get list of role names attached to this membership.\"\"\"\n",[219,4068,4069,4074,4079,4083,4087,4092,4096,4101],{"__ignoreMap":286},[318,4070,4071],{"class":320,"line":321},[318,4072,4073],{},"def is_currently_valid(self) -> bool:\n",[318,4075,4076],{"class":320,"line":327},[318,4077,4078],{},"    \"\"\"Check if membership is currently valid (time-based).\"\"\"\n",[318,4080,4081],{"class":320,"line":333},[318,4082,337],{"emptyLinePlaceholder":336},[318,4084,4085],{"class":320,"line":340},[318,4086,1750],{},[318,4088,4089],{"class":320,"line":346},[318,4090,4091],{},"    \"\"\"Check if membership can currently grant permissions (status + time).\"\"\"\n",[318,4093,4094],{"class":320,"line":352},[318,4095,337],{"emptyLinePlaceholder":336},[318,4097,4098],{"class":320,"line":358},[318,4099,4100],{},"def get_role_names(self) -> List[str]:\n",[318,4102,4103],{"class":320,"line":364},[318,4104,4105],{},"    \"\"\"Get list of role names attached to this membership.\"\"\"\n",[198,4107,4108,503],{},[210,4109,1256],{},[278,4111,4113],{"className":312,"code":4112,"language":314,"meta":286,"style":286},"# Create active membership\nmembership = EntityMembership(\n    user_id=john.id,\n    entity_id=engineering_dept.id,\n    status=MembershipStatus.ACTIVE,\n    joined_by_id=admin_user.id,\n)\nsession.add(membership)\nawait session.flush()\n\n# Attach roles via the junction table\nsession.add(EntityMembershipRole(membership_id=membership.id, role_id=manager_role.id))\nsession.add(EntityMembershipRole(membership_id=membership.id, role_id=developer_role.id))\nawait session.commit()\n\n# Revoke membership (soft delete)\nmembership.status = MembershipStatus.REVOKED\nmembership.revoked_at = datetime.now(timezone.utc)\nmembership.revoked_by_id = admin_user.id\nawait session.commit()\n",[219,4114,4115,4120,4125,4130,4135,4140,4145,4149,4154,4158,4162,4167,4172,4177,4181,4185,4190,4195,4200,4205],{"__ignoreMap":286},[318,4116,4117],{"class":320,"line":321},[318,4118,4119],{},"# Create active membership\n",[318,4121,4122],{"class":320,"line":327},[318,4123,4124],{},"membership = EntityMembership(\n",[318,4126,4127],{"class":320,"line":333},[318,4128,4129],{},"    user_id=john.id,\n",[318,4131,4132],{"class":320,"line":340},[318,4133,4134],{},"    entity_id=engineering_dept.id,\n",[318,4136,4137],{"class":320,"line":346},[318,4138,4139],{},"    status=MembershipStatus.ACTIVE,\n",[318,4141,4142],{"class":320,"line":352},[318,4143,4144],{},"    joined_by_id=admin_user.id,\n",[318,4146,4147],{"class":320,"line":358},[318,4148,1306],{},[318,4150,4151],{"class":320,"line":364},[318,4152,4153],{},"session.add(membership)\n",[318,4155,4156],{"class":320,"line":369},[318,4157,2969],{},[318,4159,4160],{"class":320,"line":374},[318,4161,337],{"emptyLinePlaceholder":336},[318,4163,4164],{"class":320,"line":380},[318,4165,4166],{},"# Attach roles via the junction table\n",[318,4168,4169],{"class":320,"line":386},[318,4170,4171],{},"session.add(EntityMembershipRole(membership_id=membership.id, role_id=manager_role.id))\n",[318,4173,4174],{"class":320,"line":392},[318,4175,4176],{},"session.add(EntityMembershipRole(membership_id=membership.id, role_id=developer_role.id))\n",[318,4178,4179],{"class":320,"line":398},[318,4180,1316],{},[318,4182,4183],{"class":320,"line":404},[318,4184,337],{"emptyLinePlaceholder":336},[318,4186,4187],{"class":320,"line":410},[318,4188,4189],{},"# Revoke membership (soft delete)\n",[318,4191,4192],{"class":320,"line":416},[318,4193,4194],{},"membership.status = MembershipStatus.REVOKED\n",[318,4196,4197],{"class":320,"line":422},[318,4198,4199],{},"membership.revoked_at = datetime.now(timezone.utc)\n",[318,4201,4202],{"class":320,"line":428},[318,4203,4204],{},"membership.revoked_by_id = admin_user.id\n",[318,4206,4207],{"class":320,"line":434},[318,4208,1316],{},[198,4210,4211,4213],{},[210,4212,3006],{},": [[54-Entity-Memberships]] for membership management.",[271,4215],{},[539,4217,4219],{"id":4218},"_9-entityclosure","9. EntityClosure",[198,4221,4222,548,4225],{},[210,4223,4224],{},"Closure table for O(1) ancestor\u002Fdescendant queries.",[219,4226,3518],{},[278,4228,4230],{"className":312,"code":4229,"language":314,"meta":286,"style":286},"class EntityClosure(BaseModel, table=True):\n    __tablename__ = \"entity_closure\"\n\n    ancestor_id: UUID     # FK -> entities.id (CASCADE)\n    descendant_id: UUID   # FK -> entities.id (CASCADE)\n\n    # Depth (0 = self, 1 = direct child, 2 = grandchild, etc.)\n    depth: int\n",[219,4231,4232,4237,4242,4246,4251,4256,4260,4265],{"__ignoreMap":286},[318,4233,4234],{"class":320,"line":321},[318,4235,4236],{},"class EntityClosure(BaseModel, table=True):\n",[318,4238,4239],{"class":320,"line":327},[318,4240,4241],{},"    __tablename__ = \"entity_closure\"\n",[318,4243,4244],{"class":320,"line":333},[318,4245,337],{"emptyLinePlaceholder":336},[318,4247,4248],{"class":320,"line":340},[318,4249,4250],{},"    ancestor_id: UUID     # FK -> entities.id (CASCADE)\n",[318,4252,4253],{"class":320,"line":346},[318,4254,4255],{},"    descendant_id: UUID   # FK -> entities.id (CASCADE)\n",[318,4257,4258],{"class":320,"line":352},[318,4259,337],{"emptyLinePlaceholder":336},[318,4261,4262],{"class":320,"line":358},[318,4263,4264],{},"    # Depth (0 = self, 1 = direct child, 2 = grandchild, etc.)\n",[318,4266,4267],{"class":320,"line":364},[318,4268,4269],{},"    depth: int\n",[198,4271,4272,503],{},[210,4273,910],{},[278,4275,4277],{"className":312,"code":4276,"language":314,"meta":286,"style":286},"UniqueConstraint(\"ancestor_id\", \"descendant_id\", name=\"uq_entity_closure\")\nIndex(\"ix_closure_ancestor_depth\", \"ancestor_id\", \"depth\")     # find descendants\nIndex(\"ix_closure_descendant_depth\", \"descendant_id\", \"depth\") # find ancestors\n",[219,4278,4279,4284,4289],{"__ignoreMap":286},[318,4280,4281],{"class":320,"line":321},[318,4282,4283],{},"UniqueConstraint(\"ancestor_id\", \"descendant_id\", name=\"uq_entity_closure\")\n",[318,4285,4286],{"class":320,"line":327},[318,4287,4288],{},"Index(\"ix_closure_ancestor_depth\", \"ancestor_id\", \"depth\")     # find descendants\n",[318,4290,4291],{"class":320,"line":333},[318,4292,4293],{},"Index(\"ix_closure_descendant_depth\", \"descendant_id\", \"depth\") # find ancestors\n",[198,4295,4296,4297,4300],{},"The composite ",[219,4298,4299],{},"(id, depth)"," indexes cover single-column lookups via their\nleading prefix, so no redundant single-column indexes exist - closure inserts\nare O(depth x subtree) rows per entity create\u002Fmove, and each extra index would\nbe paid on every one.",[198,4302,4303,503],{},[210,4304,1256],{},[278,4306,4308],{"className":312,"code":4307,"language":314,"meta":286,"style":286},"# For hierarchy: Company → Dept → Team\n# Closure records:\n[\n    (company_id, company_id, 0),  # Self\n    (company_id, dept_id, 1),     # Direct child\n    (company_id, team_id, 2),     # Grandchild\n    (dept_id, dept_id, 0),        # Self\n    (dept_id, team_id, 1),        # Direct child\n    (team_id, team_id, 0)         # Self\n]\n",[219,4309,4310,4315,4320,4325,4330,4335,4340,4345,4350,4355],{"__ignoreMap":286},[318,4311,4312],{"class":320,"line":321},[318,4313,4314],{},"# For hierarchy: Company → Dept → Team\n",[318,4316,4317],{"class":320,"line":327},[318,4318,4319],{},"# Closure records:\n",[318,4321,4322],{"class":320,"line":333},[318,4323,4324],{},"[\n",[318,4326,4327],{"class":320,"line":340},[318,4328,4329],{},"    (company_id, company_id, 0),  # Self\n",[318,4331,4332],{"class":320,"line":346},[318,4333,4334],{},"    (company_id, dept_id, 1),     # Direct child\n",[318,4336,4337],{"class":320,"line":352},[318,4338,4339],{},"    (company_id, team_id, 2),     # Grandchild\n",[318,4341,4342],{"class":320,"line":358},[318,4343,4344],{},"    (dept_id, dept_id, 0),        # Self\n",[318,4346,4347],{"class":320,"line":364},[318,4348,4349],{},"    (dept_id, team_id, 1),        # Direct child\n",[318,4351,4352],{"class":320,"line":369},[318,4353,4354],{},"    (team_id, team_id, 0)         # Self\n",[318,4356,4357],{"class":320,"line":374},[318,4358,4359],{},"]\n",[198,4361,4362,503],{},[210,4363,4364],{},"Query Examples",[278,4366,4368],{"className":312,"code":4367,"language":314,"meta":286,"style":286},"from sqlmodel import select\n\n# Get all ancestors (single indexed lookup)\nresult = await session.execute(\n    select(EntityClosure)\n    .where(EntityClosure.descendant_id == team_id, EntityClosure.depth > 0)\n    .order_by(EntityClosure.depth)\n)\nancestors = result.scalars().all()\n\n# Get all descendants\nresult = await session.execute(\n    select(EntityClosure)\n    .where(EntityClosure.ancestor_id == company_id, EntityClosure.depth > 0)\n)\ndescendants = result.scalars().all()\n\n# Check if ancestor\nresult = await session.execute(\n    select(EntityClosure).where(\n        EntityClosure.ancestor_id == company_id,\n        EntityClosure.descendant_id == team_id,\n    )\n)\nis_ancestor = result.scalar_one_or_none() is not None\n",[219,4369,4370,4375,4379,4384,4389,4394,4399,4404,4408,4413,4417,4422,4426,4430,4435,4439,4444,4448,4453,4457,4462,4467,4472,4476,4480],{"__ignoreMap":286},[318,4371,4372],{"class":320,"line":321},[318,4373,4374],{},"from sqlmodel import select\n",[318,4376,4377],{"class":320,"line":327},[318,4378,337],{"emptyLinePlaceholder":336},[318,4380,4381],{"class":320,"line":333},[318,4382,4383],{},"# Get all ancestors (single indexed lookup)\n",[318,4385,4386],{"class":320,"line":340},[318,4387,4388],{},"result = await session.execute(\n",[318,4390,4391],{"class":320,"line":346},[318,4392,4393],{},"    select(EntityClosure)\n",[318,4395,4396],{"class":320,"line":352},[318,4397,4398],{},"    .where(EntityClosure.descendant_id == team_id, EntityClosure.depth > 0)\n",[318,4400,4401],{"class":320,"line":358},[318,4402,4403],{},"    .order_by(EntityClosure.depth)\n",[318,4405,4406],{"class":320,"line":364},[318,4407,1306],{},[318,4409,4410],{"class":320,"line":369},[318,4411,4412],{},"ancestors = result.scalars().all()\n",[318,4414,4415],{"class":320,"line":374},[318,4416,337],{"emptyLinePlaceholder":336},[318,4418,4419],{"class":320,"line":380},[318,4420,4421],{},"# Get all descendants\n",[318,4423,4424],{"class":320,"line":386},[318,4425,4388],{},[318,4427,4428],{"class":320,"line":392},[318,4429,4393],{},[318,4431,4432],{"class":320,"line":398},[318,4433,4434],{},"    .where(EntityClosure.ancestor_id == company_id, EntityClosure.depth > 0)\n",[318,4436,4437],{"class":320,"line":404},[318,4438,1306],{},[318,4440,4441],{"class":320,"line":410},[318,4442,4443],{},"descendants = result.scalars().all()\n",[318,4445,4446],{"class":320,"line":416},[318,4447,337],{"emptyLinePlaceholder":336},[318,4449,4450],{"class":320,"line":422},[318,4451,4452],{},"# Check if ancestor\n",[318,4454,4455],{"class":320,"line":428},[318,4456,4388],{},[318,4458,4459],{"class":320,"line":434},[318,4460,4461],{},"    select(EntityClosure).where(\n",[318,4463,4464],{"class":320,"line":440},[318,4465,4466],{},"        EntityClosure.ancestor_id == company_id,\n",[318,4468,4469],{"class":320,"line":445},[318,4470,4471],{},"        EntityClosure.descendant_id == team_id,\n",[318,4473,4474],{"class":320,"line":451},[318,4475,407],{},[318,4477,4478],{"class":320,"line":456},[318,4479,1306],{},[318,4481,4482],{"class":320,"line":461},[318,4483,4484],{},"is_ancestor = result.scalar_one_or_none() is not None\n",[198,4486,4487,4489],{},[210,4488,3006],{},": [[53-Closure-Table]] for closure table pattern.",[271,4491],{},[539,4493,4495],{"id":4494},"_10-systemconfigmodel","10. SystemConfigModel",[198,4497,4498],{},[210,4499,4500],{},"System configuration key-value store (EnterpriseRBAC).",[278,4502,4504],{"className":312,"code":4503,"language":314,"meta":286,"style":286},"class SystemConfig(SQLModel, table=True):\n    \"\"\"System-wide configuration stored as key-value pairs.\"\"\"\n    __tablename__ = \"system_config\"\n\n    # Primary key is the config key (e.g., \"entity_types\")\n    key: str = Field(sa_column=Column(String(100), primary_key=True))\n    \n    # JSON-encoded value\n    value: str = Field(sa_column=Column(Text, nullable=False))\n    \n    # Optional description for documentation\n    description: Optional[str] = Field(default=None, sa_column=Column(Text))\n    \n    # Audit fields\n    updated_at: datetime = Field(\n        default_factory=lambda: datetime.now(timezone.utc),\n        sa_column=Column(TIMESTAMP(timezone=True), nullable=False)\n    )\n    updated_by_id: Optional[UUID] = Field(\n        default=None,\n        sa_column=Column(PG_UUID(as_uuid=True), ForeignKey(\"users.id\"))\n    )\n",[219,4505,4506,4511,4516,4521,4525,4530,4535,4540,4545,4550,4554,4559,4564,4568,4573,4577,4581,4586,4590,4595,4599,4604],{"__ignoreMap":286},[318,4507,4508],{"class":320,"line":321},[318,4509,4510],{},"class SystemConfig(SQLModel, table=True):\n",[318,4512,4513],{"class":320,"line":327},[318,4514,4515],{},"    \"\"\"System-wide configuration stored as key-value pairs.\"\"\"\n",[318,4517,4518],{"class":320,"line":333},[318,4519,4520],{},"    __tablename__ = \"system_config\"\n",[318,4522,4523],{"class":320,"line":340},[318,4524,337],{"emptyLinePlaceholder":336},[318,4526,4527],{"class":320,"line":346},[318,4528,4529],{},"    # Primary key is the config key (e.g., \"entity_types\")\n",[318,4531,4532],{"class":320,"line":352},[318,4533,4534],{},"    key: str = Field(sa_column=Column(String(100), primary_key=True))\n",[318,4536,4537],{"class":320,"line":358},[318,4538,4539],{},"    \n",[318,4541,4542],{"class":320,"line":364},[318,4543,4544],{},"    # JSON-encoded value\n",[318,4546,4547],{"class":320,"line":369},[318,4548,4549],{},"    value: str = Field(sa_column=Column(Text, nullable=False))\n",[318,4551,4552],{"class":320,"line":374},[318,4553,4539],{},[318,4555,4556],{"class":320,"line":380},[318,4557,4558],{},"    # Optional description for documentation\n",[318,4560,4561],{"class":320,"line":386},[318,4562,4563],{},"    description: Optional[str] = Field(default=None, sa_column=Column(Text))\n",[318,4565,4566],{"class":320,"line":392},[318,4567,4539],{},[318,4569,4570],{"class":320,"line":398},[318,4571,4572],{},"    # Audit fields\n",[318,4574,4575],{"class":320,"line":404},[318,4576,448],{},[318,4578,4579],{"class":320,"line":410},[318,4580,419],{},[318,4582,4583],{"class":320,"line":416},[318,4584,4585],{},"        sa_column=Column(TIMESTAMP(timezone=True), nullable=False)\n",[318,4587,4588],{"class":320,"line":422},[318,4589,407],{},[318,4591,4592],{"class":320,"line":428},[318,4593,4594],{},"    updated_by_id: Optional[UUID] = Field(\n",[318,4596,4597],{"class":320,"line":434},[318,4598,1207],{},[318,4600,4601],{"class":320,"line":440},[318,4602,4603],{},"        sa_column=Column(PG_UUID(as_uuid=True), ForeignKey(\"users.id\"))\n",[318,4605,4606],{"class":320,"line":445},[318,4607,407],{},[198,4609,4610,503],{},[210,4611,4612],{},"Configuration Keys",[278,4614,4616],{"className":312,"code":4615,"language":314,"meta":286,"style":286},"class ConfigKeys:\n    ENTITY_TYPES = \"entity_types\"  # Allowed root\u002Fchild entity types\n    FEATURE_FLAGS = \"feature_flags\"  # System feature flags (future)\n",[219,4617,4618,4623,4628],{"__ignoreMap":286},[318,4619,4620],{"class":320,"line":321},[318,4621,4622],{},"class ConfigKeys:\n",[318,4624,4625],{"class":320,"line":327},[318,4626,4627],{},"    ENTITY_TYPES = \"entity_types\"  # Allowed root\u002Fchild entity types\n",[318,4629,4630],{"class":320,"line":333},[318,4631,4632],{},"    FEATURE_FLAGS = \"feature_flags\"  # System feature flags (future)\n",[198,4634,4635,503],{},[210,4636,4637],{},"Default Entity Type Configuration",[278,4639,4641],{"className":312,"code":4640,"language":314,"meta":286,"style":286},"DEFAULT_ENTITY_TYPE_CONFIG = {\n    \"allowed_root_types\": [\"organization\"],\n    \"default_child_types\": {\n        \"structural\": [\"department\", \"team\", \"branch\"],\n        \"access_group\": [\"permission_group\", \"admin_group\"],\n    },\n}\n",[219,4642,4643,4648,4653,4658,4663,4668,4673],{"__ignoreMap":286},[318,4644,4645],{"class":320,"line":321},[318,4646,4647],{},"DEFAULT_ENTITY_TYPE_CONFIG = {\n",[318,4649,4650],{"class":320,"line":327},[318,4651,4652],{},"    \"allowed_root_types\": [\"organization\"],\n",[318,4654,4655],{"class":320,"line":333},[318,4656,4657],{},"    \"default_child_types\": {\n",[318,4659,4660],{"class":320,"line":340},[318,4661,4662],{},"        \"structural\": [\"department\", \"team\", \"branch\"],\n",[318,4664,4665],{"class":320,"line":346},[318,4666,4667],{},"        \"access_group\": [\"permission_group\", \"admin_group\"],\n",[318,4669,4670],{"class":320,"line":352},[318,4671,4672],{},"    },\n",[318,4674,4675],{"class":320,"line":358},[318,4676,4677],{},"}\n",[198,4679,4680,503],{},[210,4681,1256],{},[278,4683,4685],{"className":312,"code":4684,"language":314,"meta":286,"style":286},"# Get entity type configuration\nconfig = await config_service.get_entity_type_config(session)\n# Returns EntityTypeConfig with allowed_root_types and default_child_types\n\n# Update entity type configuration (superuser only)\nnew_config = EntityTypeConfig(\n    allowed_root_types=[\"brokerage\", \"solo_agent\", \"internal_team\"],\n    default_child_types=DefaultChildTypes(\n        structural=[\"branch\", \"team\", \"agent\"],\n        access_group=[\"permission_group\"],\n    )\n)\nawait config_service.set_entity_type_config(session, new_config, updated_by_id=user.id)\n",[219,4686,4687,4692,4697,4702,4706,4711,4716,4721,4726,4731,4736,4740,4744],{"__ignoreMap":286},[318,4688,4689],{"class":320,"line":321},[318,4690,4691],{},"# Get entity type configuration\n",[318,4693,4694],{"class":320,"line":327},[318,4695,4696],{},"config = await config_service.get_entity_type_config(session)\n",[318,4698,4699],{"class":320,"line":333},[318,4700,4701],{},"# Returns EntityTypeConfig with allowed_root_types and default_child_types\n",[318,4703,4704],{"class":320,"line":340},[318,4705,337],{"emptyLinePlaceholder":336},[318,4707,4708],{"class":320,"line":346},[318,4709,4710],{},"# Update entity type configuration (superuser only)\n",[318,4712,4713],{"class":320,"line":352},[318,4714,4715],{},"new_config = EntityTypeConfig(\n",[318,4717,4718],{"class":320,"line":358},[318,4719,4720],{},"    allowed_root_types=[\"brokerage\", \"solo_agent\", \"internal_team\"],\n",[318,4722,4723],{"class":320,"line":364},[318,4724,4725],{},"    default_child_types=DefaultChildTypes(\n",[318,4727,4728],{"class":320,"line":369},[318,4729,4730],{},"        structural=[\"branch\", \"team\", \"agent\"],\n",[318,4732,4733],{"class":320,"line":374},[318,4734,4735],{},"        access_group=[\"permission_group\"],\n",[318,4737,4738],{"class":320,"line":380},[318,4739,407],{},[318,4741,4742],{"class":320,"line":386},[318,4743,1306],{},[318,4745,4746],{"class":320,"line":392},[318,4747,4748],{},"await config_service.set_entity_type_config(session, new_config, updated_by_id=user.id)\n",[198,4750,4751,503],{},[210,4752,4753],{},"API Endpoints",[505,4755,4756,4762],{},[508,4757,4758,4761],{},[219,4759,4760],{},"GET \u002Fv1\u002Fconfig\u002Fentity-types"," - Get current configuration (public)",[508,4763,4764,4767],{},[219,4765,4766],{},"PUT \u002Fv1\u002Fconfig\u002Fentity-types"," - Update configuration (superuser only)",[198,4769,4770,4772,4773,4776],{},[210,4771,3006],{},": DD-051 in ",[219,4774,4775],{},"docs\u002FDESIGN_DECISIONS.md"," for the full design rationale.",[271,4778],{},[202,4780,4782],{"id":4781},"critical-entity-hierarchy-design-dd-050","Critical: Entity Hierarchy Design (DD-050)",[4784,4785,4786],"blockquote",{},[198,4787,4788,4791],{},[210,4789,4790],{},"⚠️ IMPORTANT",": This section describes a critical architectural constraint that affects how you should structure your entity hierarchy in EnterpriseRBAC.",[539,4793,4795],{"id":4794},"the-constraint","The Constraint",[198,4797,4798,4801,4802,4805,4806,4810,4811,226],{},[210,4799,4800],{},"Users can only belong to ONE root entity (organization)."," When a user is added as a member to any entity, they are automatically bound to that entity's root (the top-level entity with ",[219,4803,4804],{},"parent_id = NULL","). Subsequent membership additions to entities under a ",[4807,4808,4809],"em",{},"different"," root will be ",[210,4812,4813],{},"rejected",[539,4815,4817],{"id":4816},"why-this-matters","Why This Matters",[198,4819,4820],{},"If you create multiple root entities expecting users to work across them, you will encounter errors like:",[278,4822,4825],{"className":4823,"code":4824,"language":283},[281],"InvalidInputError: User belongs to a different organization\n",[219,4826,4824],{"__ignoreMap":286},[539,4828,4830],{"id":4829},"correct-vs-incorrect-hierarchy-design","Correct vs Incorrect Hierarchy Design",[278,4832,4835],{"className":4833,"code":4834,"language":283},[281],"❌ WRONG - Multiple root entities (users can't belong to both):\n\n├── ACME Realty (root, parent_id=NULL)\n│   ├── Sales Team\n│   └── Marketing Team\n│\n└── Keller Williams (root, parent_id=NULL)    \n    ├── West Branch\n    └── East Branch\n\n# Problem: A user in ACME Realty CANNOT be added to Keller Williams\n",[219,4836,4834],{"__ignoreMap":286},[278,4838,4841],{"className":4839,"code":4840,"language":283},[281],"✅ CORRECT - Single root with child organizations:\n\n└── Platform (root, parent_id=NULL)           # The \"umbrella\" entity\n    │\n    ├── ACME Realty (child organization)\n    │   ├── Sales Team\n    │   └── Marketing Team\n    │\n    ├── Keller Williams (child organization)\n    │   ├── West Branch\n    │   └── East Branch\n    │\n    └── Internal Teams (child organization)\n        ├── Engineering\n        └── Support\n\n# Solution: All entities share the same root, so users can have\n# memberships in ACME Realty AND Keller Williams AND Internal Teams\n",[219,4842,4840],{"__ignoreMap":286},[539,4844,4846],{"id":4845},"when-to-use-each-pattern","When to Use Each Pattern",[1384,4848,4849,4861],{},[1387,4850,4851],{},[1390,4852,4853,4856,4859],{},[1393,4854,4855],{},"Pattern",[1393,4857,4858],{},"Use Case",[1393,4860,1256],{},[1403,4862,4863,4876],{},[1390,4864,4865,4870,4873],{},[1408,4866,4867],{},[210,4868,4869],{},"Multiple root entities",[1408,4871,4872],{},"Completely separate organizations that should NEVER share users",[1408,4874,4875],{},"Different companies using the same SaaS platform as isolated tenants",[1390,4877,4878,4883,4886],{},[1408,4879,4880],{},[210,4881,4882],{},"Single root with children",[1408,4884,4885],{},"Related organizations where users might need access to multiple",[1408,4887,4888],{},"A platform with multiple clients, a franchise with multiple locations, a company with multiple divisions",[539,4890,4892],{"id":4891},"role-scoping-with-root-entities","Role Scoping with Root Entities",[198,4894,4895],{},"Roles can be scoped to specific root entities:",[278,4897,4899],{"className":312,"code":4898,"language":314,"meta":286,"style":286},"# Global role - available everywhere\nadmin_role = await role_service.create_role(\n    session=session,\n    name=\"admin\",\n    display_name=\"Administrator\",\n    permission_names=[\"*\"],\n    is_global=True,  # Available in ALL organizations\n    root_entity_id=None,\n)\n\n# Organization-scoped role - only available in ACME hierarchy\nacme_agent_role = await role_service.create_role(\n    session=session,\n    name=\"agent\",\n    display_name=\"Real Estate Agent\",\n    permission_names=[\"listing:*\", \"client:read\"],\n    is_global=False,\n    root_entity_id=acme_realty.id,  # Only for ACME Realty\n)\n\n# When assigning roles to memberships, only roles scoped to that\n# organization (or global roles) will be accepted\n",[219,4900,4901,4906,4911,4916,4921,4926,4931,4936,4941,4945,4949,4954,4959,4963,4968,4973,4978,4983,4988,4992,4996,5001],{"__ignoreMap":286},[318,4902,4903],{"class":320,"line":321},[318,4904,4905],{},"# Global role - available everywhere\n",[318,4907,4908],{"class":320,"line":327},[318,4909,4910],{},"admin_role = await role_service.create_role(\n",[318,4912,4913],{"class":320,"line":333},[318,4914,4915],{},"    session=session,\n",[318,4917,4918],{"class":320,"line":340},[318,4919,4920],{},"    name=\"admin\",\n",[318,4922,4923],{"class":320,"line":346},[318,4924,4925],{},"    display_name=\"Administrator\",\n",[318,4927,4928],{"class":320,"line":352},[318,4929,4930],{},"    permission_names=[\"*\"],\n",[318,4932,4933],{"class":320,"line":358},[318,4934,4935],{},"    is_global=True,  # Available in ALL organizations\n",[318,4937,4938],{"class":320,"line":364},[318,4939,4940],{},"    root_entity_id=None,\n",[318,4942,4943],{"class":320,"line":369},[318,4944,1306],{},[318,4946,4947],{"class":320,"line":374},[318,4948,337],{"emptyLinePlaceholder":336},[318,4950,4951],{"class":320,"line":380},[318,4952,4953],{},"# Organization-scoped role - only available in ACME hierarchy\n",[318,4955,4956],{"class":320,"line":386},[318,4957,4958],{},"acme_agent_role = await role_service.create_role(\n",[318,4960,4961],{"class":320,"line":392},[318,4962,4915],{},[318,4964,4965],{"class":320,"line":398},[318,4966,4967],{},"    name=\"agent\",\n",[318,4969,4970],{"class":320,"line":404},[318,4971,4972],{},"    display_name=\"Real Estate Agent\",\n",[318,4974,4975],{"class":320,"line":410},[318,4976,4977],{},"    permission_names=[\"listing:*\", \"client:read\"],\n",[318,4979,4980],{"class":320,"line":416},[318,4981,4982],{},"    is_global=False,\n",[318,4984,4985],{"class":320,"line":422},[318,4986,4987],{},"    root_entity_id=acme_realty.id,  # Only for ACME Realty\n",[318,4989,4990],{"class":320,"line":428},[318,4991,1306],{},[318,4993,4994],{"class":320,"line":434},[318,4995,337],{"emptyLinePlaceholder":336},[318,4997,4998],{"class":320,"line":440},[318,4999,5000],{},"# When assigning roles to memberships, only roles scoped to that\n",[318,5002,5003],{"class":320,"line":445},[318,5004,5005],{},"# organization (or global roles) will be accepted\n",[539,5007,5009],{"id":5008},"querying-available-roles","Querying Available Roles",[198,5011,5012],{},"Use the entity-scoped roles endpoint to get roles available for a specific entity:",[278,5014,5016],{"className":312,"code":5015,"language":314,"meta":286,"style":286},"# API: Get roles available for a specific entity\nGET \u002Fv1\u002Froles\u002Fentity\u002F{entity_id}\n\n# Returns:\n# - Global roles (is_global=True, root_entity_id=NULL)\n# - Roles scoped to the entity's root organization\n# - Entity-local roles available in the entity context\n",[219,5017,5018,5023,5028,5032,5037,5042,5047],{"__ignoreMap":286},[318,5019,5020],{"class":320,"line":321},[318,5021,5022],{},"# API: Get roles available for a specific entity\n",[318,5024,5025],{"class":320,"line":327},[318,5026,5027],{},"GET \u002Fv1\u002Froles\u002Fentity\u002F{entity_id}\n",[318,5029,5030],{"class":320,"line":333},[318,5031,337],{"emptyLinePlaceholder":336},[318,5033,5034],{"class":320,"line":340},[318,5035,5036],{},"# Returns:\n",[318,5038,5039],{"class":320,"line":346},[318,5040,5041],{},"# - Global roles (is_global=True, root_entity_id=NULL)\n",[318,5043,5044],{"class":320,"line":352},[318,5045,5046],{},"# - Roles scoped to the entity's root organization\n",[318,5048,5049],{"class":320,"line":358},[318,5050,5051],{},"# - Entity-local roles available in the entity context\n",[539,5053,5055],{"id":5054},"migration-considerations","Migration Considerations",[198,5057,5058],{},"If you have an existing deployment with multiple root entities and need to restructure:",[5060,5061,5062,5065,5068,5071],"ol",{},[508,5063,5064],{},"Create a new umbrella root entity",[508,5066,5067],{},"Update existing \"root\" entities to have the new umbrella as their parent",[508,5069,5070],{},"Update closure table entries",[508,5072,5073,5074,5077],{},"Users' ",[219,5075,5076],{},"root_entity_id"," will need to be updated to the new umbrella",[198,5079,5080,5082,5083,4776],{},[210,5081,3006],{},": DD-050 in ",[219,5084,4775],{},[271,5086],{},[202,5088,5090],{"id":5089},"abac-models","ABAC Models",[539,5092,5094],{"id":5093},"_10-condition","10. Condition",[198,5096,5097],{},[210,5098,5099],{},"Embedded model for ABAC conditions.",[278,5101,5103],{"className":312,"code":5102,"language":314,"meta":286,"style":286},"class ConditionOperator(str, Enum):\n    # Equality\n    EQUALS = \"equals\"\n    NOT_EQUALS = \"not_equals\"\n\n    # Comparison\n    LESS_THAN = \"less_than\"\n    LESS_THAN_OR_EQUAL = \"less_than_or_equal\"\n    GREATER_THAN = \"greater_than\"\n    GREATER_THAN_OR_EQUAL = \"greater_than_or_equal\"\n\n    # Collection\n    IN = \"in\"\n    NOT_IN = \"not_in\"\n    CONTAINS = \"contains\"\n    NOT_CONTAINS = \"not_contains\"\n\n    # String\n    STARTS_WITH = \"starts_with\"\n    ENDS_WITH = \"ends_with\"\n    MATCHES = \"matches\"  # Regex\n\n    # Existence\n    EXISTS = \"exists\"\n    NOT_EXISTS = \"not_exists\"\n\n    # Boolean\n    IS_TRUE = \"is_true\"\n    IS_FALSE = \"is_false\"\n\n    # Time\n    BEFORE = \"before\"\n    AFTER = \"after\"\n\nclass Condition(BaseModel):\n    attribute: str  # e.g., \"resource.department\", \"user.role\"\n    operator: ConditionOperator\n    value: Optional[Union[str, int, float, bool, List[Any]]] = None\n    description: Optional[str] = None\n",[219,5104,5105,5110,5115,5120,5125,5129,5134,5139,5144,5149,5154,5158,5163,5168,5173,5178,5183,5187,5192,5197,5202,5207,5211,5216,5221,5226,5230,5235,5240,5245,5249,5254,5259,5264,5268,5273,5278,5283,5288],{"__ignoreMap":286},[318,5106,5107],{"class":320,"line":321},[318,5108,5109],{},"class ConditionOperator(str, Enum):\n",[318,5111,5112],{"class":320,"line":327},[318,5113,5114],{},"    # Equality\n",[318,5116,5117],{"class":320,"line":333},[318,5118,5119],{},"    EQUALS = \"equals\"\n",[318,5121,5122],{"class":320,"line":340},[318,5123,5124],{},"    NOT_EQUALS = \"not_equals\"\n",[318,5126,5127],{"class":320,"line":346},[318,5128,337],{"emptyLinePlaceholder":336},[318,5130,5131],{"class":320,"line":352},[318,5132,5133],{},"    # Comparison\n",[318,5135,5136],{"class":320,"line":358},[318,5137,5138],{},"    LESS_THAN = \"less_than\"\n",[318,5140,5141],{"class":320,"line":364},[318,5142,5143],{},"    LESS_THAN_OR_EQUAL = \"less_than_or_equal\"\n",[318,5145,5146],{"class":320,"line":369},[318,5147,5148],{},"    GREATER_THAN = \"greater_than\"\n",[318,5150,5151],{"class":320,"line":374},[318,5152,5153],{},"    GREATER_THAN_OR_EQUAL = \"greater_than_or_equal\"\n",[318,5155,5156],{"class":320,"line":380},[318,5157,337],{"emptyLinePlaceholder":336},[318,5159,5160],{"class":320,"line":386},[318,5161,5162],{},"    # Collection\n",[318,5164,5165],{"class":320,"line":392},[318,5166,5167],{},"    IN = \"in\"\n",[318,5169,5170],{"class":320,"line":398},[318,5171,5172],{},"    NOT_IN = \"not_in\"\n",[318,5174,5175],{"class":320,"line":404},[318,5176,5177],{},"    CONTAINS = \"contains\"\n",[318,5179,5180],{"class":320,"line":410},[318,5181,5182],{},"    NOT_CONTAINS = \"not_contains\"\n",[318,5184,5185],{"class":320,"line":416},[318,5186,337],{"emptyLinePlaceholder":336},[318,5188,5189],{"class":320,"line":422},[318,5190,5191],{},"    # String\n",[318,5193,5194],{"class":320,"line":428},[318,5195,5196],{},"    STARTS_WITH = \"starts_with\"\n",[318,5198,5199],{"class":320,"line":434},[318,5200,5201],{},"    ENDS_WITH = \"ends_with\"\n",[318,5203,5204],{"class":320,"line":440},[318,5205,5206],{},"    MATCHES = \"matches\"  # Regex\n",[318,5208,5209],{"class":320,"line":445},[318,5210,337],{"emptyLinePlaceholder":336},[318,5212,5213],{"class":320,"line":451},[318,5214,5215],{},"    # Existence\n",[318,5217,5218],{"class":320,"line":456},[318,5219,5220],{},"    EXISTS = \"exists\"\n",[318,5222,5223],{"class":320,"line":461},[318,5224,5225],{},"    NOT_EXISTS = \"not_exists\"\n",[318,5227,5228],{"class":320,"line":466},[318,5229,337],{"emptyLinePlaceholder":336},[318,5231,5232],{"class":320,"line":472},[318,5233,5234],{},"    # Boolean\n",[318,5236,5237],{"class":320,"line":687},[318,5238,5239],{},"    IS_TRUE = \"is_true\"\n",[318,5241,5242],{"class":320,"line":692},[318,5243,5244],{},"    IS_FALSE = \"is_false\"\n",[318,5246,5247],{"class":320,"line":698},[318,5248,337],{"emptyLinePlaceholder":336},[318,5250,5251],{"class":320,"line":704},[318,5252,5253],{},"    # Time\n",[318,5255,5256],{"class":320,"line":710},[318,5257,5258],{},"    BEFORE = \"before\"\n",[318,5260,5261],{"class":320,"line":716},[318,5262,5263],{},"    AFTER = \"after\"\n",[318,5265,5266],{"class":320,"line":722},[318,5267,337],{"emptyLinePlaceholder":336},[318,5269,5270],{"class":320,"line":727},[318,5271,5272],{},"class Condition(BaseModel):\n",[318,5274,5275],{"class":320,"line":733},[318,5276,5277],{},"    attribute: str  # e.g., \"resource.department\", \"user.role\"\n",[318,5279,5280],{"class":320,"line":739},[318,5281,5282],{},"    operator: ConditionOperator\n",[318,5284,5285],{"class":320,"line":745},[318,5286,5287],{},"    value: Optional[Union[str, int, float, bool, List[Any]]] = None\n",[318,5289,5290],{"class":320,"line":751},[318,5291,1519],{},[198,5293,5294,503],{},[210,5295,1256],{},[278,5297,5299],{"className":312,"code":5298,"language":314,"meta":286,"style":286},"# Department-based access\nCondition(\n    attribute=\"resource.department\",\n    operator=ConditionOperator.EQUALS,\n    value=\"engineering\"\n)\n\n# Budget threshold\nCondition(\n    attribute=\"resource.amount\",\n    operator=ConditionOperator.LESS_THAN,\n    value=100000\n)\n\n# Business hours\nCondition(\n    attribute=\"time.hour\",\n    operator=ConditionOperator.GREATER_THAN_OR_EQUAL,\n    value=9\n)\n",[219,5300,5301,5306,5311,5316,5321,5326,5330,5334,5339,5343,5348,5353,5358,5362,5366,5371,5375,5380,5385,5390],{"__ignoreMap":286},[318,5302,5303],{"class":320,"line":321},[318,5304,5305],{},"# Department-based access\n",[318,5307,5308],{"class":320,"line":327},[318,5309,5310],{},"Condition(\n",[318,5312,5313],{"class":320,"line":333},[318,5314,5315],{},"    attribute=\"resource.department\",\n",[318,5317,5318],{"class":320,"line":340},[318,5319,5320],{},"    operator=ConditionOperator.EQUALS,\n",[318,5322,5323],{"class":320,"line":346},[318,5324,5325],{},"    value=\"engineering\"\n",[318,5327,5328],{"class":320,"line":352},[318,5329,1306],{},[318,5331,5332],{"class":320,"line":358},[318,5333,337],{"emptyLinePlaceholder":336},[318,5335,5336],{"class":320,"line":364},[318,5337,5338],{},"# Budget threshold\n",[318,5340,5341],{"class":320,"line":369},[318,5342,5310],{},[318,5344,5345],{"class":320,"line":374},[318,5346,5347],{},"    attribute=\"resource.amount\",\n",[318,5349,5350],{"class":320,"line":380},[318,5351,5352],{},"    operator=ConditionOperator.LESS_THAN,\n",[318,5354,5355],{"class":320,"line":386},[318,5356,5357],{},"    value=100000\n",[318,5359,5360],{"class":320,"line":392},[318,5361,1306],{},[318,5363,5364],{"class":320,"line":398},[318,5365,337],{"emptyLinePlaceholder":336},[318,5367,5368],{"class":320,"line":404},[318,5369,5370],{},"# Business hours\n",[318,5372,5373],{"class":320,"line":410},[318,5374,5310],{},[318,5376,5377],{"class":320,"line":416},[318,5378,5379],{},"    attribute=\"time.hour\",\n",[318,5381,5382],{"class":320,"line":422},[318,5383,5384],{},"    operator=ConditionOperator.GREATER_THAN_OR_EQUAL,\n",[318,5386,5387],{"class":320,"line":428},[318,5388,5389],{},"    value=9\n",[318,5391,5392],{"class":320,"line":434},[318,5393,1306],{},[198,5395,5396,888,5398,5401],{},[210,5397,3006],{},[235,5399,5400],{"href":108},"26 — ABAC"," for the implementer guide.",[271,5403],{},[539,5405,5407],{"id":5406},"_11-conditiongroup","11. ConditionGroup",[198,5409,5410],{},[210,5411,5412],{},"Group of conditions with logical operators.",[278,5414,5416],{"className":312,"code":5415,"language":314,"meta":286,"style":286},"class ConditionGroup(BaseModel):\n    conditions: List[Condition] = Field(min_length=1)\n    operator: str = \"AND\"  # \"AND\" or \"OR\"\n    description: Optional[str] = None\n",[219,5417,5418,5423,5428,5433],{"__ignoreMap":286},[318,5419,5420],{"class":320,"line":321},[318,5421,5422],{},"class ConditionGroup(BaseModel):\n",[318,5424,5425],{"class":320,"line":327},[318,5426,5427],{},"    conditions: List[Condition] = Field(min_length=1)\n",[318,5429,5430],{"class":320,"line":333},[318,5431,5432],{},"    operator: str = \"AND\"  # \"AND\" or \"OR\"\n",[318,5434,5435],{"class":320,"line":340},[318,5436,1519],{},[198,5438,5439,503],{},[210,5440,1256],{},[278,5442,5444],{"className":312,"code":5443,"language":314,"meta":286,"style":286},"# Manager in engineering department\nConditionGroup(\n    conditions=[\n        Condition(attribute=\"user.role\", operator=\"equals\", value=\"manager\"),\n        Condition(attribute=\"user.department\", operator=\"equals\", value=\"engineering\")\n    ],\n    operator=\"AND\"\n)\n\n# Admin OR owner\nConditionGroup(\n    conditions=[\n        Condition(attribute=\"user.role\", operator=\"equals\", value=\"admin\"),\n        Condition(attribute=\"user.role\", operator=\"equals\", value=\"owner\")\n    ],\n    operator=\"OR\"\n)\n",[219,5445,5446,5451,5456,5461,5466,5471,5476,5481,5485,5489,5494,5498,5502,5507,5512,5516,5521],{"__ignoreMap":286},[318,5447,5448],{"class":320,"line":321},[318,5449,5450],{},"# Manager in engineering department\n",[318,5452,5453],{"class":320,"line":327},[318,5454,5455],{},"ConditionGroup(\n",[318,5457,5458],{"class":320,"line":333},[318,5459,5460],{},"    conditions=[\n",[318,5462,5463],{"class":320,"line":340},[318,5464,5465],{},"        Condition(attribute=\"user.role\", operator=\"equals\", value=\"manager\"),\n",[318,5467,5468],{"class":320,"line":346},[318,5469,5470],{},"        Condition(attribute=\"user.department\", operator=\"equals\", value=\"engineering\")\n",[318,5472,5473],{"class":320,"line":352},[318,5474,5475],{},"    ],\n",[318,5477,5478],{"class":320,"line":358},[318,5479,5480],{},"    operator=\"AND\"\n",[318,5482,5483],{"class":320,"line":364},[318,5484,1306],{},[318,5486,5487],{"class":320,"line":369},[318,5488,337],{"emptyLinePlaceholder":336},[318,5490,5491],{"class":320,"line":374},[318,5492,5493],{},"# Admin OR owner\n",[318,5495,5496],{"class":320,"line":380},[318,5497,5455],{},[318,5499,5500],{"class":320,"line":386},[318,5501,5460],{},[318,5503,5504],{"class":320,"line":392},[318,5505,5506],{},"        Condition(attribute=\"user.role\", operator=\"equals\", value=\"admin\"),\n",[318,5508,5509],{"class":320,"line":398},[318,5510,5511],{},"        Condition(attribute=\"user.role\", operator=\"equals\", value=\"owner\")\n",[318,5513,5514],{"class":320,"line":404},[318,5515,5475],{},[318,5517,5518],{"class":320,"line":410},[318,5519,5520],{},"    operator=\"OR\"\n",[318,5522,5523],{"class":320,"line":416},[318,5524,1306],{},[271,5526],{},[202,5528,5530],{"id":5529},"oauth-models","OAuth Models",[539,5532,5534],{"id":5533},"_12-oauthstate","12. OAuthState",[198,5536,5537],{},[210,5538,5539],{},"OAuth state parameter storage (JWT-based alternative available).",[278,5541,5543],{"className":312,"code":5542,"language":314,"meta":286,"style":286},"class OAuthState(Document):\n    state: str  # Unique state parameter\n    provider: str  # \"google\", \"github\", etc.\n\n    # PKCE\n    code_verifier: Optional[str] = None\n    code_challenge: Optional[str] = None\n\n    # OIDC\n    nonce: Optional[str] = None\n\n    # Redirect\n    redirect_uri: str\n\n    # User context (for manual linking)\n    user_id: Optional[UUID] = None\n\n    # Security\n    ip_address: Optional[str] = None\n    user_agent: Optional[str] = None\n\n    # Expiration\n    expires_at: datetime\n    created_at: datetime = Field(default_factory=datetime.utcnow)\n",[219,5544,5545,5550,5555,5560,5564,5569,5574,5579,5583,5588,5593,5597,5602,5607,5611,5616,5621,5625,5630,5634,5638,5642,5646,5650],{"__ignoreMap":286},[318,5546,5547],{"class":320,"line":321},[318,5548,5549],{},"class OAuthState(Document):\n",[318,5551,5552],{"class":320,"line":327},[318,5553,5554],{},"    state: str  # Unique state parameter\n",[318,5556,5557],{"class":320,"line":333},[318,5558,5559],{},"    provider: str  # \"google\", \"github\", etc.\n",[318,5561,5562],{"class":320,"line":340},[318,5563,337],{"emptyLinePlaceholder":336},[318,5565,5566],{"class":320,"line":346},[318,5567,5568],{},"    # PKCE\n",[318,5570,5571],{"class":320,"line":352},[318,5572,5573],{},"    code_verifier: Optional[str] = None\n",[318,5575,5576],{"class":320,"line":358},[318,5577,5578],{},"    code_challenge: Optional[str] = None\n",[318,5580,5581],{"class":320,"line":364},[318,5582,337],{"emptyLinePlaceholder":336},[318,5584,5585],{"class":320,"line":369},[318,5586,5587],{},"    # OIDC\n",[318,5589,5590],{"class":320,"line":374},[318,5591,5592],{},"    nonce: Optional[str] = None\n",[318,5594,5595],{"class":320,"line":380},[318,5596,337],{"emptyLinePlaceholder":336},[318,5598,5599],{"class":320,"line":386},[318,5600,5601],{},"    # Redirect\n",[318,5603,5604],{"class":320,"line":392},[318,5605,5606],{},"    redirect_uri: str\n",[318,5608,5609],{"class":320,"line":398},[318,5610,337],{"emptyLinePlaceholder":336},[318,5612,5613],{"class":320,"line":404},[318,5614,5615],{},"    # User context (for manual linking)\n",[318,5617,5618],{"class":320,"line":410},[318,5619,5620],{},"    user_id: Optional[UUID] = None\n",[318,5622,5623],{"class":320,"line":416},[318,5624,337],{"emptyLinePlaceholder":336},[318,5626,5627],{"class":320,"line":422},[318,5628,5629],{},"    # Security\n",[318,5631,5632],{"class":320,"line":428},[318,5633,2242],{},[318,5635,5636],{"class":320,"line":434},[318,5637,2247],{},[318,5639,5640],{"class":320,"line":440},[318,5641,337],{"emptyLinePlaceholder":336},[318,5643,5644],{"class":320,"line":445},[318,5645,2189],{},[318,5647,5648],{"class":320,"line":451},[318,5649,2194],{},[318,5651,5652],{"class":320,"line":456},[318,5653,5654],{},"    created_at: datetime = Field(default_factory=datetime.utcnow)\n",[198,5656,5657,5659,5660,5663],{},[210,5658,2091],{},": OutlabsAuth uses ",[210,5661,5662],{},"JWT-based state tokens"," (DD-042) by default instead of database storage. This model is for reference.",[271,5665],{},[539,5667,5669],{"id":5668},"_13-activitymetric","13. ActivityMetric",[198,5671,5672],{},[210,5673,5674],{},"Historical snapshots for DAU\u002FMAU\u002FWAU\u002FQAU tracking (DD-049).",[278,5676,5678],{"className":312,"code":5677,"language":314,"meta":286,"style":286},"class ActivityMetric(BaseModel, table=True):\n    __tablename__ = \"activity_metrics\"\n\n    metric_date: date       # Date for this metric\n    metric_type: str        # \"dau\", \"mau\", \"qau\", \"logins\", \"registrations\", \"api_calls\"\n    count: int = 0          # Count for this metric\n    unique_users: int = 0   # Unique user count (for DAU\u002FMAU)\n    snapshot_at: datetime   # When this snapshot was taken\n",[219,5679,5680,5685,5690,5694,5699,5704,5709,5714],{"__ignoreMap":286},[318,5681,5682],{"class":320,"line":321},[318,5683,5684],{},"class ActivityMetric(BaseModel, table=True):\n",[318,5686,5687],{"class":320,"line":327},[318,5688,5689],{},"    __tablename__ = \"activity_metrics\"\n",[318,5691,5692],{"class":320,"line":333},[318,5693,337],{"emptyLinePlaceholder":336},[318,5695,5696],{"class":320,"line":340},[318,5697,5698],{},"    metric_date: date       # Date for this metric\n",[318,5700,5701],{"class":320,"line":346},[318,5702,5703],{},"    metric_type: str        # \"dau\", \"mau\", \"qau\", \"logins\", \"registrations\", \"api_calls\"\n",[318,5705,5706],{"class":320,"line":352},[318,5707,5708],{},"    count: int = 0          # Count for this metric\n",[318,5710,5711],{"class":320,"line":358},[318,5712,5713],{},"    unique_users: int = 0   # Unique user count (for DAU\u002FMAU)\n",[318,5715,5716],{"class":320,"line":364},[318,5717,5718],{},"    snapshot_at: datetime   # When this snapshot was taken\n",[198,5720,5721,503],{},[210,5722,910],{},[278,5724,5726],{"className":312,"code":5725,"language":314,"meta":286,"style":286},"UniqueConstraint(\"metric_date\", \"metric_type\", name=\"uq_activity_metric_date_type\")\nIndex(\"ix_activity_metrics_date\", \"metric_date\")\nIndex(\"ix_activity_metrics_type\", \"metric_type\")\n",[219,5727,5728,5733,5738],{"__ignoreMap":286},[318,5729,5730],{"class":320,"line":321},[318,5731,5732],{},"UniqueConstraint(\"metric_date\", \"metric_type\", name=\"uq_activity_metric_date_type\")\n",[318,5734,5735],{"class":320,"line":327},[318,5736,5737],{},"Index(\"ix_activity_metrics_date\", \"metric_date\")\n",[318,5739,5740],{"class":320,"line":333},[318,5741,5742],{},"Index(\"ix_activity_metrics_type\", \"metric_type\")\n",[198,5744,5745,5748],{},[210,5746,5747],{},"Purpose",": Historical activity tracking for analytics and growth monitoring.",[198,5750,5751,503],{},[210,5752,5753],{},"Data Flow",[5060,5755,5756,5763,5773],{},[508,5757,5758,5759,5762],{},"Real-time tracking in ",[210,5760,5761],{},"Redis Sets"," (O(1) operations)",[508,5764,5765,5768,5769,5772],{},[219,5766,5767],{},"ActivityTracker.sync_to_database()"," upserts one row per\n",[219,5770,5771],{},"(metric_date, metric_type)"," on the sync interval (default: 30 minutes)",[508,5774,5775],{},"Rows older than 90 days are deleted on each sync",[198,5777,5778,5779,5782],{},"The table stores ",[210,5780,5781],{},"counts only"," - there is no user-id column, so\ncohort\u002Fretention analysis is not supported from this table.",[198,5784,5785,888,5787,5790],{},[210,5786,3006],{},[219,5788,5789],{},"docs-library\u002F49-Activity-Tracking.md"," for complete implementation details.",[271,5792],{},[539,5794,5796],{"id":5795},"_14-useractivity-and-loginhistory","14. UserActivity and LoginHistory",[198,5798,5799,5800,5803,5804,5807,5808,5811],{},"Two further activity tables are ",[210,5801,5802],{},"declared and migrated, but no library service\nwrites to them"," - ",[219,5805,5806],{},"ActivityTracker"," only writes ",[219,5809,5810],{},"activity_metrics",". They are\navailable for host applications to populate.",[278,5813,5815],{"className":312,"code":5814,"language":314,"meta":286,"style":286},"class UserActivity(BaseModel, table=True):\n    __tablename__ = \"user_activities\"\n\n    user_id: UUID           # FK -> users.id (CASCADE)\n    activity_date: date\n    login_count: int = 0\n    api_call_count: int = 0\n    action_count: int = 0\n    first_activity_at: datetime\n    last_activity_at: datetime\n\nclass LoginHistory(BaseModel, table=True):\n    __tablename__ = \"login_history\"\n\n    user_id: UUID           # FK -> users.id (CASCADE)\n    login_at: datetime\n    success: bool\n    auth_method: str        # password, oauth, api_key, ...\n    ip_address: Optional[str] = None\n    user_agent: Optional[str] = None\n    device_fingerprint: Optional[str] = None\n    country_code: Optional[str] = None\n    city: Optional[str] = None\n    failure_reason: Optional[str] = None\n",[219,5816,5817,5822,5827,5831,5836,5841,5846,5851,5856,5861,5866,5870,5875,5880,5884,5888,5893,5898,5903,5907,5911,5916,5921,5926],{"__ignoreMap":286},[318,5818,5819],{"class":320,"line":321},[318,5820,5821],{},"class UserActivity(BaseModel, table=True):\n",[318,5823,5824],{"class":320,"line":327},[318,5825,5826],{},"    __tablename__ = \"user_activities\"\n",[318,5828,5829],{"class":320,"line":333},[318,5830,337],{"emptyLinePlaceholder":336},[318,5832,5833],{"class":320,"line":340},[318,5834,5835],{},"    user_id: UUID           # FK -> users.id (CASCADE)\n",[318,5837,5838],{"class":320,"line":346},[318,5839,5840],{},"    activity_date: date\n",[318,5842,5843],{"class":320,"line":352},[318,5844,5845],{},"    login_count: int = 0\n",[318,5847,5848],{"class":320,"line":358},[318,5849,5850],{},"    api_call_count: int = 0\n",[318,5852,5853],{"class":320,"line":364},[318,5854,5855],{},"    action_count: int = 0\n",[318,5857,5858],{"class":320,"line":369},[318,5859,5860],{},"    first_activity_at: datetime\n",[318,5862,5863],{"class":320,"line":374},[318,5864,5865],{},"    last_activity_at: datetime\n",[318,5867,5868],{"class":320,"line":380},[318,5869,337],{"emptyLinePlaceholder":336},[318,5871,5872],{"class":320,"line":386},[318,5873,5874],{},"class LoginHistory(BaseModel, table=True):\n",[318,5876,5877],{"class":320,"line":392},[318,5878,5879],{},"    __tablename__ = \"login_history\"\n",[318,5881,5882],{"class":320,"line":398},[318,5883,337],{"emptyLinePlaceholder":336},[318,5885,5886],{"class":320,"line":404},[318,5887,5835],{},[318,5889,5890],{"class":320,"line":410},[318,5891,5892],{},"    login_at: datetime\n",[318,5894,5895],{"class":320,"line":416},[318,5896,5897],{},"    success: bool\n",[318,5899,5900],{"class":320,"line":422},[318,5901,5902],{},"    auth_method: str        # password, oauth, api_key, ...\n",[318,5904,5905],{"class":320,"line":428},[318,5906,2242],{},[318,5908,5909],{"class":320,"line":434},[318,5910,2247],{},[318,5912,5913],{"class":320,"line":440},[318,5914,5915],{},"    device_fingerprint: Optional[str] = None\n",[318,5917,5918],{"class":320,"line":445},[318,5919,5920],{},"    country_code: Optional[str] = None\n",[318,5922,5923],{"class":320,"line":451},[318,5924,5925],{},"    city: Optional[str] = None\n",[318,5927,5928],{"class":320,"line":456},[318,5929,5930],{},"    failure_reason: Optional[str] = None\n",[271,5932],{},[202,5934,5936],{"id":5935},"table-summary","Table Summary",[198,5938,5939,5941],{},[219,5940,256],{}," is the authority for which tables a given\nconfiguration creates. The base set is created for every configuration; the\nhierarchy flag then switches the last group.",[198,5943,5944,503],{},[210,5945,5946],{},"Always created",[1384,5948,5949,5958],{},[1387,5950,5951],{},[1390,5952,5953,5956],{},[1393,5954,5955],{},"Table",[1393,5957,5747],{},[1403,5959,5960,5969,5978,5987,5996,6006,6015,6024,6034,6043,6052,6061],{},[1390,5961,5962,5966],{},[1408,5963,5964],{},[210,5965,551],{},[1408,5967,5968],{},"User accounts",[1390,5970,5971,5975],{},[1408,5972,5973],{},[210,5974,1376],{},[1408,5976,5977],{},"Role definitions",[1390,5979,5980,5984],{},[1408,5981,5982],{},[210,5983,1943],{},[1408,5985,5986],{},"Permission registry",[1390,5988,5989,5993],{},[1408,5990,5991],{},[210,5992,891],{},[1408,5994,5995],{},"JWT refresh tokens",[1390,5997,5998,6003],{},[1408,5999,6000],{},[210,6001,6002],{},"integration_principals",[1408,6004,6005],{},"Non-human (system integration) principals",[1390,6007,6008,6012],{},[1408,6009,6010],{},[210,6011,895],{},[1408,6013,6014],{},"API key authentication",[1390,6016,6017,6021],{},[1408,6018,6019],{},[210,6020,898],{},[1408,6022,6023],{},"OAuth accounts",[1390,6025,6026,6031],{},[1408,6027,6028],{},[210,6029,6030],{},"oauth_states",[1408,6032,6033],{},"OAuth state storage",[1390,6035,6036,6040],{},[1408,6037,6038],{},[210,6039,5810],{},[1408,6041,6042],{},"DAU\u002FMAU tracking (DD-049)",[1390,6044,6045,6049],{},[1408,6046,6047],{},[210,6048,1472],{},[1408,6050,6051],{},"Role definition change history",[1390,6053,6054,6058],{},[1408,6055,6056],{},[210,6057,2085],{},[1408,6059,6060],{},"Permission definition change history",[1390,6062,6063,6068],{},[1408,6064,6065],{},[210,6066,6067],{},"user_audit_events",[1408,6069,6070],{},"User audit log",[198,6072,6073,6078],{},[210,6074,6075,6076],{},"With ",[219,6077,264],{}," (EnterpriseRBAC):",[1384,6080,6081,6089],{},[1387,6082,6083],{},[1390,6084,6085,6087],{},[1393,6086,5955],{},[1393,6088,5747],{},[1403,6090,6091,6100,6110,6119,6128],{},[1390,6092,6093,6097],{},[1408,6094,6095],{},[210,6096,3267],{},[1408,6098,6099],{},"Entity hierarchy",[1390,6101,6102,6107],{},[1408,6103,6104],{},[210,6105,6106],{},"entity_memberships",[1408,6108,6109],{},"User memberships",[1390,6111,6112,6116],{},[1408,6113,6114],{},[210,6115,3875],{},[1408,6117,6118],{},"Membership -> Role junction",[1390,6120,6121,6125],{},[1408,6122,6123],{},[210,6124,3518],{},[1408,6126,6127],{},"Closure table",[1390,6129,6130,6134],{},[1408,6131,6132],{},[210,6133,4055],{},[1408,6135,6136],{},"Membership change history",[198,6138,6139,6144],{},[210,6140,6075,6141],{},[219,6142,6143],{},"enable_entity_hierarchy=False"," (SimpleRBAC):",[1384,6146,6147,6155],{},[1387,6148,6149],{},[1390,6150,6151,6153],{},[1393,6152,5955],{},[1393,6154,5747],{},[1403,6156,6157],{},[1390,6158,6159,6163],{},[1408,6160,6161],{},[210,6162,268],{},[1408,6164,6165],{},"Direct user -> role assignment",[198,6167,6168,6169,899,6171,892,6173,892,6175,899,6177,892,6179,892,6181,899,6183,892,6185,892,6187,899,6189,892,6192,892,6195,899,6198,892,6201,6204,6205,226],{},"Supporting tables backing the models above - ",[219,6170,1412],{},[219,6172,1427],{},[219,6174,1442],{},[219,6176,1457],{},[219,6178,2073],{},[219,6180,2077],{},[219,6182,2081],{},[219,6184,2487],{},[219,6186,2502],{},[219,6188,2517],{},[219,6190,6191],{},"integration_principal_roles",[219,6193,6194],{},"auth_challenges",[219,6196,6197],{},"system_config",[219,6199,6200],{},"user_activities",[219,6202,6203],{},"login_history"," - bring the full schema to ",[210,6206,6207],{},"33 tables",[271,6209],{},[202,6211,6213],{"id":6212},"indexes-summary","Indexes Summary",[539,6215,6217],{"id":6216},"critical-indexes","Critical Indexes",[198,6219,6220,503],{},[210,6221,1134],{},[278,6223,6225],{"className":312,"code":6224,"language":314,"meta":286,"style":286},"UniqueConstraint(\"email\", name=\"uq_users_email\")   # UNIQUE — also the email index\nIndex(\"ix_users_status\", \"status\")                 # Filter by status\n",[219,6226,6227,6232],{"__ignoreMap":286},[318,6228,6229],{"class":320,"line":321},[318,6230,6231],{},"UniqueConstraint(\"email\", name=\"uq_users_email\")   # UNIQUE — also the email index\n",[318,6233,6234],{"class":320,"line":327},[318,6235,6236],{},"Index(\"ix_users_status\", \"status\")                 # Filter by status\n",[198,6238,6239,503],{},[210,6240,6241],{},"Role",[278,6243,6245],{"className":312,"code":6244,"language":314,"meta":286,"style":286},"Index(\"ix_roles_name\", \"name\")                     # Role name lookup (not unique)\n",[219,6246,6247],{"__ignoreMap":286},[318,6248,6249],{"class":320,"line":321},[318,6250,6244],{},[198,6252,6253,503],{},[210,6254,6255],{},"APIKey",[278,6257,6259],{"className":312,"code":6258,"language":314,"meta":286,"style":286},"UniqueConstraint(\"prefix\", name=\"uq_api_keys_prefix\")  # UNIQUE — prefix lookup\nIndex(\"ix_api_keys_owner_id\", \"owner_id\")              # User's API keys\n",[219,6260,6261,6266],{"__ignoreMap":286},[318,6262,6263],{"class":320,"line":321},[318,6264,6265],{},"UniqueConstraint(\"prefix\", name=\"uq_api_keys_prefix\")  # UNIQUE — prefix lookup\n",[318,6267,6268],{"class":320,"line":327},[318,6269,6270],{},"Index(\"ix_api_keys_owner_id\", \"owner_id\")              # User's API keys\n",[198,6272,6273,503],{},[210,6274,6275],{},"SocialAccount",[278,6277,6279],{"className":312,"code":6278,"language":314,"meta":286,"style":286},"UniqueConstraint(\"provider\", \"provider_user_id\", name=\"uq_social_provider_user\")\nIndex(\"ix_social_accounts_user_id\", \"user_id\")     # User's OAuth accounts\n",[219,6280,6281,6285],{"__ignoreMap":286},[318,6282,6283],{"class":320,"line":321},[318,6284,3173],{},[318,6286,6287],{"class":320,"line":327},[318,6288,6289],{},"Index(\"ix_social_accounts_user_id\", \"user_id\")     # User's OAuth accounts\n",[198,6291,6292,6078],{},[210,6293,3741],{},[278,6295,6297],{"className":312,"code":6296,"language":314,"meta":286,"style":286},"UniqueConstraint(\"slug\", name=\"uq_entities_slug\")  # UNIQUE — slug lookup\nIndex(\"ix_entities_parent_id\", \"parent_id\")        # Hierarchy queries\n",[219,6298,6299,6304],{"__ignoreMap":286},[318,6300,6301],{"class":320,"line":321},[318,6302,6303],{},"UniqueConstraint(\"slug\", name=\"uq_entities_slug\")  # UNIQUE — slug lookup\n",[318,6305,6306],{"class":320,"line":327},[318,6307,6308],{},"Index(\"ix_entities_parent_id\", \"parent_id\")        # Hierarchy queries\n",[198,6310,6311,6078],{},[210,6312,6313],{},"EntityMembership",[278,6315,6317],{"className":312,"code":6316,"language":314,"meta":286,"style":286},"UniqueConstraint(\"user_id\", \"entity_id\", name=\"uq_entity_membership\")\nIndex(\"ix_em_entity_id\", \"entity_id\")              # Entity members\nIndex(\"ix_em_user_status\", \"user_id\", \"status\")    # User memberships by status\n",[219,6318,6319,6323,6328],{"__ignoreMap":286},[318,6320,6321],{"class":320,"line":321},[318,6322,4029],{},[318,6324,6325],{"class":320,"line":327},[318,6326,6327],{},"Index(\"ix_em_entity_id\", \"entity_id\")              # Entity members\n",[318,6329,6330],{"class":320,"line":333},[318,6331,6332],{},"Index(\"ix_em_user_status\", \"user_id\", \"status\")    # User memberships by status\n",[198,6334,6335,6078],{},[210,6336,6337],{},"EntityClosure",[278,6339,6341],{"className":312,"code":6340,"language":314,"meta":286,"style":286},"UniqueConstraint(\"ancestor_id\", \"descendant_id\", name=\"uq_entity_closure\")\nIndex(\"ix_closure_descendant_depth\", \"descendant_id\", \"depth\")  # Ancestor queries\nIndex(\"ix_closure_ancestor_depth\", \"ancestor_id\", \"depth\")      # Descendant queries\n",[219,6342,6343,6347,6352],{"__ignoreMap":286},[318,6344,6345],{"class":320,"line":321},[318,6346,4283],{},[318,6348,6349],{"class":320,"line":327},[318,6350,6351],{},"Index(\"ix_closure_descendant_depth\", \"descendant_id\", \"depth\")  # Ancestor queries\n",[318,6353,6354],{"class":320,"line":333},[318,6355,6356],{},"Index(\"ix_closure_ancestor_depth\", \"ancestor_id\", \"depth\")      # Descendant queries\n",[198,6358,6359,6362,6363,6366,6367,226],{},[210,6360,6361],{},"Note on index hygiene",": several \"obvious\" single-column indexes are\ndeliberately absent - a ",[219,6364,6365],{},"UniqueConstraint"," already materializes a btree, and\ncomposite indexes cover single-column lookups through their leading prefix. See\nmigration ",[219,6368,6369],{},"20260611_0018_index_hygiene.py",[271,6371],{},[202,6373,6375],{"id":6374},"database-initialization","Database Initialization",[539,6377,6379],{"id":6378},"setup-with-sqlalchemysqlmodel","Setup with SQLAlchemy\u002FSQLModel",[278,6381,6383],{"className":312,"code":6382,"language":314,"meta":286,"style":286},"from contextlib import asynccontextmanager\nfrom sqlmodel import SQLModel\nfrom outlabs_auth import SimpleRBAC  # or EnterpriseRBAC\n\nDATABASE_URL = \"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Fmydb\"\nSECRET_KEY = \"your-secret-key\"\n\n@asynccontextmanager\nasync def lifespan(app):\n    # Initialize auth\n    auth = SimpleRBAC(database_url=DATABASE_URL, secret_key=SECRET_KEY)\n    await auth.initialize()\n\n    # Create all tables\n    async with auth.engine.begin() as conn:\n        await conn.run_sync(SQLModel.metadata.create_all)\n\n    yield\n\n    await auth.shutdown()\n",[219,6384,6385,6390,6395,6400,6404,6409,6414,6418,6423,6428,6433,6438,6443,6447,6452,6457,6462,6466,6471,6475],{"__ignoreMap":286},[318,6386,6387],{"class":320,"line":321},[318,6388,6389],{},"from contextlib import asynccontextmanager\n",[318,6391,6392],{"class":320,"line":327},[318,6393,6394],{},"from sqlmodel import SQLModel\n",[318,6396,6397],{"class":320,"line":333},[318,6398,6399],{},"from outlabs_auth import SimpleRBAC  # or EnterpriseRBAC\n",[318,6401,6402],{"class":320,"line":340},[318,6403,337],{"emptyLinePlaceholder":336},[318,6405,6406],{"class":320,"line":346},[318,6407,6408],{},"DATABASE_URL = \"postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Fmydb\"\n",[318,6410,6411],{"class":320,"line":352},[318,6412,6413],{},"SECRET_KEY = \"your-secret-key\"\n",[318,6415,6416],{"class":320,"line":358},[318,6417,337],{"emptyLinePlaceholder":336},[318,6419,6420],{"class":320,"line":364},[318,6421,6422],{},"@asynccontextmanager\n",[318,6424,6425],{"class":320,"line":369},[318,6426,6427],{},"async def lifespan(app):\n",[318,6429,6430],{"class":320,"line":374},[318,6431,6432],{},"    # Initialize auth\n",[318,6434,6435],{"class":320,"line":380},[318,6436,6437],{},"    auth = SimpleRBAC(database_url=DATABASE_URL, secret_key=SECRET_KEY)\n",[318,6439,6440],{"class":320,"line":386},[318,6441,6442],{},"    await auth.initialize()\n",[318,6444,6445],{"class":320,"line":392},[318,6446,337],{"emptyLinePlaceholder":336},[318,6448,6449],{"class":320,"line":398},[318,6450,6451],{},"    # Create all tables\n",[318,6453,6454],{"class":320,"line":404},[318,6455,6456],{},"    async with auth.engine.begin() as conn:\n",[318,6458,6459],{"class":320,"line":410},[318,6460,6461],{},"        await conn.run_sync(SQLModel.metadata.create_all)\n",[318,6463,6464],{"class":320,"line":416},[318,6465,337],{"emptyLinePlaceholder":336},[318,6467,6468],{"class":320,"line":422},[318,6469,6470],{},"    yield\n",[318,6472,6473],{"class":320,"line":428},[318,6474,337],{"emptyLinePlaceholder":336},[318,6476,6477],{"class":320,"line":434},[318,6478,6479],{},"    await auth.shutdown()\n",[539,6481,6483],{"id":6482},"initialize-with-outlabsauth","Initialize with OutlabsAuth",[278,6485,6487],{"className":312,"code":6486,"language":314,"meta":286,"style":286},"from outlabs_auth import SimpleRBAC  # or EnterpriseRBAC\n\nauth = SimpleRBAC(\n    database_url=\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost:5432\u002Fmydb\",\n    secret_key=\"your-secret-key\"\n)\nawait auth.initialize()  # Creates engine and session factory\n",[219,6488,6489,6493,6497,6502,6507,6512,6516],{"__ignoreMap":286},[318,6490,6491],{"class":320,"line":321},[318,6492,6399],{},[318,6494,6495],{"class":320,"line":327},[318,6496,337],{"emptyLinePlaceholder":336},[318,6498,6499],{"class":320,"line":333},[318,6500,6501],{},"auth = SimpleRBAC(\n",[318,6503,6504],{"class":320,"line":340},[318,6505,6506],{},"    database_url=\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost:5432\u002Fmydb\",\n",[318,6508,6509],{"class":320,"line":346},[318,6510,6511],{},"    secret_key=\"your-secret-key\"\n",[318,6513,6514],{"class":320,"line":352},[318,6515,1306],{},[318,6517,6518],{"class":320,"line":358},[318,6519,6520],{},"await auth.initialize()  # Creates engine and session factory\n",[271,6522],{},[202,6524,5055],{"id":6525},"migration-considerations-1",[539,6527,6529],{"id":6528},"from-centralized-api","From Centralized API",[198,6531,6532],{},"If migrating from OutlabsAuth's old centralized API:",[198,6534,6535,503],{},[210,6536,6537],{},"Changed",[505,6539,6540,6547,6554],{},[508,6541,6542,6543,6546],{},"❌ Removed ",[219,6544,6545],{},"platform_id"," (no multi-platform)",[508,6548,6549,6550,6553],{},"✅ Added optional ",[219,6551,6552],{},"tenant_id"," (for multi-tenant mode)",[508,6555,6556],{},"✅ Simplified entity hierarchy (single collection)",[198,6558,6559,503],{},[210,6560,6561],{},"Preserved",[505,6563,6564,6567,6570,6573],{},[508,6565,6566],{},"✅ User model structure",[508,6568,6569],{},"✅ Role\u002Fpermission model",[508,6571,6572],{},"✅ Entity hierarchy structure",[508,6574,6575],{},"✅ Closure table pattern",[539,6577,6579],{"id":6578},"schema-versioning","Schema Versioning",[198,6581,6582,6583,6586,6587,6590,6591,6594,6595,269],{},"Schema changes are managed with ",[210,6584,6585],{},"Alembic",". Migrations live in\n",[219,6588,6589],{},"outlabs_auth\u002Fmigrations\u002Fversions\u002F"," and are named ",[219,6592,6593],{},"YYYYMMDD_NNNN_\u003Cslug>.py","\n(for example ",[219,6596,6597],{},"20260715_0020_add_refresh_token_families.py",[198,6599,6600,6601,6604,6605,226],{},"Hosts can either run migrations themselves or pass ",[219,6602,6603],{},"auto_migrate=True"," to\n",[219,6606,6607],{},"OutlabsAuth",[271,6609],{},[202,6611,6613],{"id":6612},"best-practices","Best Practices",[539,6615,6617],{"id":6616},"_1-use-indexes","1. Use Indexes",[278,6619,6621],{"className":312,"code":6620,"language":314,"meta":286,"style":286},"from sqlmodel import select\n\n# ✅ Good - unique\u002Findexed column\nresult = await session.execute(select(User).where(User.email == email))\nuser = result.scalar_one_or_none()\n\n# ❌ Bad - unindexed column forces a sequential scan\nresult = await session.execute(select(User).where(User.timezone == tz))\n",[219,6622,6623,6627,6631,6636,6641,6646,6650,6655],{"__ignoreMap":286},[318,6624,6625],{"class":320,"line":321},[318,6626,4374],{},[318,6628,6629],{"class":320,"line":327},[318,6630,337],{"emptyLinePlaceholder":336},[318,6632,6633],{"class":320,"line":333},[318,6634,6635],{},"# ✅ Good - unique\u002Findexed column\n",[318,6637,6638],{"class":320,"line":340},[318,6639,6640],{},"result = await session.execute(select(User).where(User.email == email))\n",[318,6642,6643],{"class":320,"line":346},[318,6644,6645],{},"user = result.scalar_one_or_none()\n",[318,6647,6648],{"class":320,"line":352},[318,6649,337],{"emptyLinePlaceholder":336},[318,6651,6652],{"class":320,"line":358},[318,6653,6654],{},"# ❌ Bad - unindexed column forces a sequential scan\n",[318,6656,6657],{"class":320,"line":364},[318,6658,6659],{},"result = await session.execute(select(User).where(User.timezone == tz))\n",[539,6661,6663],{"id":6662},"_2-avoid-n1-queries","2. Avoid N+1 Queries",[278,6665,6667],{"className":312,"code":6666,"language":314,"meta":286,"style":286},"from sqlalchemy.orm import selectinload\n\n# ❌ Bad - lazy-loads each user separately\nresult = await session.execute(select(EntityMembership))\nfor membership in result.scalars():\n    user = membership.user  # N queries\n\n# ✅ Good - eager-load the relationship in one round trip\nresult = await session.execute(\n    select(EntityMembership).options(selectinload(EntityMembership.user))\n)\n",[219,6668,6669,6674,6678,6683,6688,6693,6698,6702,6707,6711,6716],{"__ignoreMap":286},[318,6670,6671],{"class":320,"line":321},[318,6672,6673],{},"from sqlalchemy.orm import selectinload\n",[318,6675,6676],{"class":320,"line":327},[318,6677,337],{"emptyLinePlaceholder":336},[318,6679,6680],{"class":320,"line":333},[318,6681,6682],{},"# ❌ Bad - lazy-loads each user separately\n",[318,6684,6685],{"class":320,"line":340},[318,6686,6687],{},"result = await session.execute(select(EntityMembership))\n",[318,6689,6690],{"class":320,"line":346},[318,6691,6692],{},"for membership in result.scalars():\n",[318,6694,6695],{"class":320,"line":352},[318,6696,6697],{},"    user = membership.user  # N queries\n",[318,6699,6700],{"class":320,"line":358},[318,6701,337],{"emptyLinePlaceholder":336},[318,6703,6704],{"class":320,"line":364},[318,6705,6706],{},"# ✅ Good - eager-load the relationship in one round trip\n",[318,6708,6709],{"class":320,"line":369},[318,6710,4388],{},[318,6712,6713],{"class":320,"line":374},[318,6714,6715],{},"    select(EntityMembership).options(selectinload(EntityMembership.user))\n",[318,6717,6718],{"class":320,"line":380},[318,6719,1306],{},[539,6721,6723],{"id":6722},"_3-let-the-model-validate","3. Let the Model Validate",[278,6725,6727],{"className":312,"code":6726,"language":314,"meta":286,"style":286},"# ✅ Good - SQLModel validates on construction\nuser = User(email=\"user@example.com\", ...)\nsession.add(user)\nawait session.commit()\n\n# ❌ Bad - skips validation\nuser = User.model_construct(email=\"invalid\")\n",[219,6728,6729,6734,6739,6743,6747,6751,6756],{"__ignoreMap":286},[318,6730,6731],{"class":320,"line":321},[318,6732,6733],{},"# ✅ Good - SQLModel validates on construction\n",[318,6735,6736],{"class":320,"line":327},[318,6737,6738],{},"user = User(email=\"user@example.com\", ...)\n",[318,6740,6741],{"class":320,"line":333},[318,6742,1311],{},[318,6744,6745],{"class":320,"line":340},[318,6746,1316],{},[318,6748,6749],{"class":320,"line":346},[318,6750,337],{"emptyLinePlaceholder":336},[318,6752,6753],{"class":320,"line":352},[318,6754,6755],{},"# ❌ Bad - skips validation\n",[318,6757,6758],{"class":320,"line":358},[318,6759,6760],{},"user = User.model_construct(email=\"invalid\")\n",[539,6762,6764],{"id":6763},"_4-use-transactions-for-critical-operations","4. Use Transactions for Critical Operations",[198,6766,6767],{},"The session is the transaction - commit once, so a failure rolls back the whole\nunit of work.",[278,6769,6771],{"className":312,"code":6770,"language":314,"meta":286,"style":286},"# ✅ Good - entity + membership commit together or not at all\nasync with auth.get_session() as session:\n    entity = Entity(...)\n    session.add(entity)\n    await session.flush()          # populate entity.id without committing\n\n    session.add(EntityMembership(entity_id=entity.id, ...))\n    await session.commit()         # single atomic commit\n",[219,6772,6773,6778,6783,6788,6793,6798,6802,6807],{"__ignoreMap":286},[318,6774,6775],{"class":320,"line":321},[318,6776,6777],{},"# ✅ Good - entity + membership commit together or not at all\n",[318,6779,6780],{"class":320,"line":327},[318,6781,6782],{},"async with auth.get_session() as session:\n",[318,6784,6785],{"class":320,"line":333},[318,6786,6787],{},"    entity = Entity(...)\n",[318,6789,6790],{"class":320,"line":340},[318,6791,6792],{},"    session.add(entity)\n",[318,6794,6795],{"class":320,"line":346},[318,6796,6797],{},"    await session.flush()          # populate entity.id without committing\n",[318,6799,6800],{"class":320,"line":352},[318,6801,337],{"emptyLinePlaceholder":336},[318,6803,6804],{"class":320,"line":358},[318,6805,6806],{},"    session.add(EntityMembership(entity_id=entity.id, ...))\n",[318,6808,6809],{"class":320,"line":364},[318,6810,6811],{},"    await session.commit()         # single atomic commit\n",[271,6813],{},[202,6815,6817],{"id":6816},"summary","Summary",[1384,6819,6820,6830],{},[1387,6821,6822],{},[1390,6823,6824,6827],{},[1393,6825,6826],{},"Area",[1393,6828,6829],{},"Models",[1403,6831,6832,6855,6867],{},[1390,6833,6834,6837],{},[1408,6835,6836],{},"Auth core",[1408,6838,6839,892,6841,892,6843,892,6845,892,6848,892,6850,892,6852],{},[219,6840,1134],{},[219,6842,6241],{},[219,6844,2094],{},[219,6846,6847],{},"RefreshToken",[219,6849,6255],{},[219,6851,6275],{},[219,6853,6854],{},"IntegrationPrincipal",[1390,6856,6857,6859],{},[1408,6858,117],{},[1408,6860,6861,892,6863,892,6865],{},[219,6862,3741],{},[219,6864,6313],{},[219,6866,6337],{},[1390,6868,6869,6871],{},[1408,6870,107],{},[1408,6872,6873,892,6875,892,6878],{},[219,6874,1432],{},[219,6876,6877],{},"PermissionCondition",[219,6879,1462],{},[198,6881,6882,6885,6886,226],{},[210,6883,6884],{},"Database:"," PostgreSQL + SQLAlchemy\u002FSQLModel (asyncpg). Table subset depends on\nfeature flags. Migrations: ",[219,6887,6589],{},[271,6889],{},[202,6891,6893],{"id":6892},"related","Related",[505,6895,6896,6900,6904,6908,6912,6916],{},[508,6897,6898],{},[235,6899,5],{"href":16},[508,6901,6902],{},[235,6903,123],{"href":124},[508,6905,6906],{},[235,6907,87],{"href":88},[508,6909,6910],{},[235,6911,128],{"href":129},[508,6913,6914],{},[235,6915,107],{"href":108},[508,6917,6918,6919],{},"Maintainer architecture: ",[235,6920,6921],{"href":124},[219,6922,6923],{},"docs\u002FLIBRARY_ARCHITECTURE.md",[539,6925,6927],{"id":6926},"further-reading","Further reading",[505,6929,6930,6938,6951],{},[508,6931,6932],{},[235,6933,6937],{"href":6934,"rel":6935},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002F",[6936],"nofollow","PostgreSQL docs",[508,6939,6940,6945,6946],{},[235,6941,6944],{"href":6942,"rel":6943},"https:\u002F\u002Fdocs.sqlalchemy.org\u002F",[6936],"SQLAlchemy"," · ",[235,6947,6950],{"href":6948,"rel":6949},"https:\u002F\u002Fsqlmodel.tiangolo.com\u002F",[6936],"SQLModel",[508,6952,6953,6958],{},[235,6954,6957],{"href":6955,"rel":6956},"https:\u002F\u002Fwww.slideshare.net\u002Fbillkarwin\u002Fmodels-for-hierarchical-data",[6936],"Closure table pattern"," (hierarchy background)",[6960,6961,6962],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":286,"searchDepth":321,"depth":327,"links":6964},[6965,6966,6967,6968,6976,6982,6991,6995,7000,7001,7004,7008,7012,7018,7019],{"id":204,"depth":327,"text":205},{"id":275,"depth":327,"text":276},{"id":291,"depth":327,"text":292},{"id":536,"depth":327,"text":537,"children":6969},[6970,6971,6972,6973,6974,6975],{"id":541,"depth":333,"text":542},{"id":1367,"depth":333,"text":1368},{"id":1934,"depth":333,"text":1935},{"id":2103,"depth":333,"text":2104},{"id":2451,"depth":333,"text":2452},{"id":3012,"depth":333,"text":3013},{"id":3254,"depth":327,"text":3255,"children":6977},[6978,6979,6980,6981],{"id":3258,"depth":333,"text":3259},{"id":3863,"depth":333,"text":3864},{"id":4218,"depth":333,"text":4219},{"id":4494,"depth":333,"text":4495},{"id":4781,"depth":327,"text":4782,"children":6983},[6984,6985,6986,6987,6988,6989,6990],{"id":4794,"depth":333,"text":4795},{"id":4816,"depth":333,"text":4817},{"id":4829,"depth":333,"text":4830},{"id":4845,"depth":333,"text":4846},{"id":4891,"depth":333,"text":4892},{"id":5008,"depth":333,"text":5009},{"id":5054,"depth":333,"text":5055},{"id":5089,"depth":327,"text":5090,"children":6992},[6993,6994],{"id":5093,"depth":333,"text":5094},{"id":5406,"depth":333,"text":5407},{"id":5529,"depth":327,"text":5530,"children":6996},[6997,6998,6999],{"id":5533,"depth":333,"text":5534},{"id":5668,"depth":333,"text":5669},{"id":5795,"depth":333,"text":5796},{"id":5935,"depth":327,"text":5936},{"id":6212,"depth":327,"text":6213,"children":7002},[7003],{"id":6216,"depth":333,"text":6217},{"id":6374,"depth":327,"text":6375,"children":7005},[7006,7007],{"id":6378,"depth":333,"text":6379},{"id":6482,"depth":333,"text":6483},{"id":6525,"depth":327,"text":5055,"children":7009},[7010,7011],{"id":6528,"depth":333,"text":6529},{"id":6578,"depth":333,"text":6579},{"id":6612,"depth":327,"text":6613,"children":7013},[7014,7015,7016,7017],{"id":6616,"depth":333,"text":6617},{"id":6662,"depth":333,"text":6663},{"id":6722,"depth":333,"text":6723},{"id":6763,"depth":333,"text":6764},{"id":6816,"depth":327,"text":6817},{"id":6892,"depth":327,"text":6893,"children":7020},[7021],{"id":6926,"depth":333,"text":6927},"Postgres \u002F SQLModel schema reference.","md",null,{},{"icon":163},{"title":160,"description":7022},"RF_HmlNa7KGkCDxOLzGo_Ud4raJmfCbJa_Jeo25elYM",[7030,7032],{"title":149,"path":150,"stem":151,"description":7031,"icon":152,"children":-1},"Optional sidecar admin console — point it at any FastAPI host running OutlabsAuth and manage the full surface.",{"title":165,"path":166,"stem":167,"description":7033,"icon":168,"children":-1},"DAU \u002F MAU style engagement counters.",1787472745731]