PRE-RELEASE INFORMATION: SUBJECT TO CHANGE

Understanding Housecarl: A Domain-Driven Design Perspective

This document explains the conceptual model behind Housecarl AuthZ. Understanding these concepts will help you design effective authorization policies and integrate Housecarl into your applications.

Hands-on learning: As you read through these concepts, open the Console to see them in action. Each section notes where you can explore that concept in the UI.


The Core Problem

Authorization answers a deceptively simple question: "Can this user do this action on this resource?"

In practice, this becomes complex:

  • Users belong to organizations with different permission models
  • Resources have hierarchical relationships
  • Policies need to be inherited, overridden, and composed
  • Multiple teams need to manage their own access rules
  • Everything must be auditable and secure

Housecarl addresses this complexity through a carefully designed domain model.


Bounded Contexts

Housecarl operates within three distinct bounded contexts:

1. Identity Context

What it answers: "Who is this person?"

This context handles:

  • User authentication (passwords, OAuth, API keys)
  • Session management (JWT tokens)
  • User profiles and credentials

In the Console: See this in the Users section where you manage user accounts and their authentication methods.

2. Authorization Context

What it answers: "What can this person do?"

This is the heart of Housecarl:

  • Policy definition and evaluation
  • Domain hierarchies and inheritance
  • Resource protection

In the Console: Explore this in the Domains section where you define policies.

3. Tenancy Context

What it answers: "Whose system is this?"

This context provides:

  • Multi-tenant isolation
  • Organizational boundaries
  • Resource quotas and billing

In the Console: Manage this in the Tenants section.


Aggregate Roots

In Domain-Driven Design, an aggregate root is the primary entity that owns and protects a cluster of related objects. Housecarl has three aggregate roots:

Tenant (The Organizational Boundary)

A Tenant represents an isolated organizational unit—typically a company, team, or project that uses Housecarl.

Tenant
├── identity: UUID (globally unique)
├── name: String (globally unique, human-readable)
├── description: String
├── domains: [Domain, Domain, ...]  ← contains Domains
├── rate_limits: TokenBucket
└── active: Boolean

Key invariants:

  • Tenant names are globally unique (you can't have two "acme-corp" tenants)
  • All resources belong to exactly one tenant
  • Users can be associated with multiple tenants
  • A tenant's data is completely isolated from other tenants

Why this matters: When you create a tenant in the Console, you're creating a security boundary. Resources in Tenant A cannot be accessed using policies from Tenant B—this is enforced at the core of the authorization engine.

In the Console: Create and manage tenants in the Tenants section. Notice how switching tenants changes your entire view of domains and policies.


Domain (The Policy Container)

A Domain is a logical grouping of policies that protect a category of resources. Think of it as a "policy folder" with inheritance capabilities.

Domain
├── identity: UUID
├── name: String (unique within tenant)
├── tenant_id: UUID (belongs to one tenant)
├── superior_domains: [Domain, ...]  ← inheritance
├── policies: [Policy, ...]  ← authorization rules
└── active: Boolean

Key invariants:

  • Domain names are unique within a tenant (but different tenants can have same-named domains)
  • Domains form a directed acyclic graph (DAG) through superior domain relationships
  • Policies are inherited from all superior domains
  • A domain belongs to exactly one tenant

The inheritance model:

                    ┌─────────────┐
                    │   global    │  ← base policies everyone inherits
                    └──────┬──────┘
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌─────────┐  ┌─────────┐  ┌─────────┐
        │  team-a │  │  team-b │  │  admins │
        └────┬────┘  └────┬────┘  └─────────┘
             │            │
             ▼            ▼
       ┌──────────┐ ┌──────────┐
       │ project-x│ │ project-y│  ← inherit from team + global
       └──────────┘ └──────────┘

When evaluating authorization for project-x, Housecarl collects policies from:

  1. project-x itself
  2. team-a (its superior)
  3. global (team-a's superior)

All matching policies must agree—if any denies access, access is denied.

In the Console: In the Domains section, you can see the domain hierarchy and configure superior domain relationships. The policy inheritance is visualized to help you understand which policies apply.


User (The Identity)

A User represents an authenticated identity that can make requests.

User
├── identity: UUID
├── username: String (globally unique)
├── email: String (globally unique)
├── password: HashedPassword (Argon2)
├── email_verified: Boolean
├── tenants: [Tenant, ...]  ← can belong to multiple
├── api_keys: [ApiKey, ...]
└── active: Boolean

Key invariants:

  • Usernames and emails are globally unique
  • Users can belong to multiple tenants (multi-tenancy support)
  • Passwords are never stored in plain text (Argon2 hashing)
  • API keys provide non-interactive authentication

In the Console: Manage users in the Users section. You can see which tenants a user belongs to and manage their API keys.


Value Objects

Value objects are immutable concepts defined by their attributes rather than identity. They're the building blocks of authorization decisions.

Policy (The Authorization Rule)

A Policy defines conditions under which access is granted or denied.

Policy
├── name: String
├── description: String (optional)
├── engine: EvaluationEngine
├── statements: [Statement, ...]
├── deny: Boolean (deny vs allow)
└── invert: Boolean (flip the result)

Evaluation engines:

EngineMatchingExample Use Case
FixedExact string matchSpecific resource access
PrefixString starts-withHierarchical paths
RegExRegular expressionPattern matching
GlobShell-style wildcardsFile-like paths

Deny semantics: By default, policies grant access when they match. Setting deny: true creates a deny policy—if it matches, access is explicitly refused regardless of other policies.

Policy composition:

Request comes in
Collect all policies from domain + superiors
Evaluate each policy against request
If ANY deny policy matches → DENIED
If ALL allow policies match → GRANTED
If NO policy matches → DENIED (default deny)

In the Console: In the Domains section, click on a domain to see and edit its policies. The policy editor helps you build statements with the right engine.


Request (The Authorization Question)

A Request encapsulates everything needed to make an authorization decision.

Request
├── subject: String (who is asking)
├── action: String (what they want to do)
├── object: Resource (what they want to do it to)
└── context: Map<String, String> (additional attributes)

The canonical question: "Can subject perform action on object?"

Example:

{
  "subject": "alice",
  "action": "read",
  "object": "hc://documents/project-x/budget.xlsx",
  "context": {
    "department": "engineering",
    "clearance": "confidential"
  }
}

The context map carries additional attributes that policies can match against—department, role, time of day, IP address, or any custom attribute your application provides.


Resource (The Protected Thing)

Resources use a URI format that enables hierarchical matching:

hc://noun/noun_id/segment1/segment2/...

Components:

  • hc:// — the scheme (always "hc" for Housecarl)
  • noun — the resource type (e.g., "documents", "users", "api")
  • noun_id — the specific instance
  • segments — hierarchical path within the resource

Examples:

hc://tenant/acme-corp                    # The ACME Corp tenant
hc://documents/project-x/reports/q4.pdf  # A specific document
hc://api/users/alice/profile             # Alice's profile via API
hc://domain/engineering/policies         # Engineering domain's policies

This hierarchical structure enables powerful policy patterns:

  • Match all documents: hc://documents/.*
  • Match project-x documents: hc://documents/project-x/.*
  • Match only PDFs: hc://documents/.*/.*\.pdf

Statement (The Matching Rule)

A Statement is a set of key-value conditions that must all match for the policy to apply.

Statement
└── rules: Map<String, String>

Example statement (using RegEx engine):

{
  "subject": "alice|bob|charlie",
  "action": "read|write",
  "object": "hc://documents/team-alpha/.*"
}

This matches if:

  • Subject is alice, bob, OR charlie
  • Action is read OR write
  • Object is anything under hc://documents/team-alpha/

All conditions must match: If a request has subject=alice, action=read, but object=hc://documents/team-beta/secret.pdf, this statement does NOT match (object doesn't match the pattern).


The Macro System

Policies support dynamic values through macros—placeholders that are expanded at evaluation time.

MacroExpands ToUse Case
$current_user()Authenticated usernameUser-specific resources
$requestors_tenant()Caller's tenant IDTenant isolation
$current_time()Unix timestampTime-based access
$resource_tenant()Target resource's tenantCross-tenant checks
$resource_path()Resource path segmentsPath-based matching
$resource_type()Resource nounType-based policies

Example: Allow users to access only their own profile:

{
  "subject": "$current_user()",
  "action": "read|write",
  "object": "hc://users/$current_user()/profile"
}

When Alice makes a request, $current_user() expands to alice, so she can only access her own profile.


Multi-Tenancy Model

Housecarl's multi-tenancy is fundamental to its security model. Here's how it works:

Tenant Isolation

When a request comes in for a resource, Housecarl determines which tenant's policies to evaluate:

Request for: hc://tenant/acme-corp/documents/secret.pdf
                        └── This tells us to use ACME Corp's policies

The key insight: We evaluate using the target tenant's policies, not the requesting user's tenant policies.

This means:

  • A user from Tenant A requesting access to Tenant B's resources is evaluated against Tenant B's policies
  • Tenant B's policies don't know about Tenant A's users
  • Therefore, cross-tenant access is denied by default

This is "secure by architecture"—you don't need to write policies to prevent cross-tenant access; it's impossible unless explicitly configured.

User-Tenant Association

Users can belong to multiple tenants:

User: alice@example.com
├── Member of: acme-corp (admin role)
├── Member of: beta-inc (viewer role)
└── Member of: personal-project (owner role)

When Alice authenticates, her JWT token includes which tenant context she's operating in. Switching tenants in the Console changes this context.

In the Console: The tenant selector in the header shows which tenant context you're operating in. Your permissions change based on the selected tenant.


Authorization Decision Flow

Here's the complete flow when Housecarl evaluates an authorization request:

┌─────────────────────────────────────────────────────────────────┐
│ 1. REQUEST ARRIVES                                              │
│    Subject: alice                                                │
│    Action: write                                                 │
│    Object: hc://documents/project-x/report.pdf                  │
│    Context: {department: engineering, clearance: secret}        │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 2. TENANT RESOLUTION                                            │
│    Parse resource → determine target tenant                      │
│    Load tenant's domain structure                                │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 3. POLICY COLLECTION                                            │
│    Start at resource's domain (e.g., "project-x")               │
│    Traverse superior domains (team-a → global)                  │
│    Collect all policies from traversal                          │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 4. MACRO EXPANSION                                              │
│    $current_user() → "alice"                                    │
│    $current_time() → 1704067200                                 │
│    etc.                                                          │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 5. POLICY EVALUATION                                            │
│    For each policy:                                              │
│      For each statement:                                         │
│        Match all conditions against request                      │
│        Apply evaluation engine (Fixed/Prefix/RegEx/Glob)        │
│      If any statement matches → policy matches                  │
│    Combine results: ALL must allow, ANY deny blocks             │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 6. DECISION                                                     │
│    ✓ ALLOWED: All matching policies grant access                │
│    ✗ DENIED: Any deny policy matched, or no policy matched      │
└─────────────────────────────────────────────────────────────────┘

Common Patterns

Pattern 1: Role-Based Access Control (RBAC)

Create domains for each role, with appropriate inheritance:

global (base permissions everyone gets)
├── viewer (read-only access)
├── editor (read + write)
│   └── inherits: viewer
└── admin (full access)
    └── inherits: editor

Assign users to domains based on their role.

Pattern 2: Resource Hierarchies

Use the resource path for hierarchical permissions:

// Policy in "documents" domain
{
  "subject": ".*",
  "action": "read",
  "object": "hc://documents/public/.*"
}

Anyone can read public documents. More specific domains can grant access to restricted areas.

Pattern 3: Attribute-Based Access Control (ABAC)

Use context attributes for fine-grained control:

{
  "subject": ".*",
  "action": "read",
  "object": "hc://documents/classified/.*",
  "clearance": "top-secret"
}

Only users whose request includes clearance: top-secret can access classified documents.

Pattern 4: Time-Based Access

Use the time macro for temporal restrictions:

{
  "subject": "contractor-.*",
  "action": ".*",
  "object": "hc://systems/production/.*",
  "time_allowed": "true"  // Application must validate business hours
}

Note: Time validation often requires application-level logic to set context appropriately.

Pattern 5: Self-Service Resources

Let users manage their own resources:

{
  "subject": "$current_user()",
  "action": "read|write|delete",
  "object": "hc://users/$current_user()/.*"
}

Alice can manage anything under hc://users/alice/, but not Bob's resources.


Summary

Housecarl's domain model provides:

ConceptPurposeConsole Location
TenantOrganizational isolationTenants section
DomainPolicy grouping + inheritanceDomains section
UserAuthenticated identityUsers section
PolicyAuthorization rulesDomain detail view
RequestAuthorization questionAPI / housectl
ResourceProtected thing (URI format)In your application

The model is designed for:

  • Security: Multi-tenant isolation by architecture
  • Flexibility: Policy inheritance and composition
  • Clarity: Explicit rules with predictable evaluation
  • Auditability: Every decision is traceable

Ready to put this into practice? Head to the Console and start building your authorization model, or check out the Policy Cookbook for more examples.