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.
Authorization answers a deceptively simple question: "Can this user do this action on this resource?"
In practice, this becomes complex:
Housecarl addresses this complexity through a carefully designed domain model.
Housecarl operates within three distinct bounded contexts:
What it answers: "Who is this person?"
This context handles:
In the Console: See this in the Users section where you manage user accounts and their authentication methods.
What it answers: "What can this person do?"
This is the heart of Housecarl:
In the Console: Explore this in the Domains section where you define policies.
What it answers: "Whose system is this?"
This context provides:
In the Console: Manage this in the Tenants section.
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:
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:
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.
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:
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:
project-x itselfteam-a (its superior)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.
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:
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 are immutable concepts defined by their attributes rather than identity. They're the building blocks of authorization decisions.
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:
| Engine | Matching | Example Use Case |
|---|---|---|
Fixed | Exact string match | Specific resource access |
Prefix | String starts-with | Hierarchical paths |
RegEx | Regular expression | Pattern matching |
Glob | Shell-style wildcards | File-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.
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.
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 instancesegments — hierarchical path within the resourceExamples:
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:
hc://documents/.*hc://documents/project-x/.*hc://documents/.*/.*\.pdfA 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:
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).
Policies support dynamic values through macros—placeholders that are expanded at evaluation time.
| Macro | Expands To | Use Case |
|---|---|---|
$current_user() | Authenticated username | User-specific resources |
$requestors_tenant() | Caller's tenant ID | Tenant isolation |
$current_time() | Unix timestamp | Time-based access |
$resource_tenant() | Target resource's tenant | Cross-tenant checks |
$resource_path() | Resource path segments | Path-based matching |
$resource_type() | Resource noun | Type-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.
Housecarl's multi-tenancy is fundamental to its security model. Here's how it works:
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:
This is "secure by architecture"—you don't need to write policies to prevent cross-tenant access; it's impossible unless explicitly configured.
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.
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 │
└─────────────────────────────────────────────────────────────────┘
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.
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.
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.
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.
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.
Housecarl's domain model provides:
| Concept | Purpose | Console Location |
|---|---|---|
| Tenant | Organizational isolation | Tenants section |
| Domain | Policy grouping + inheritance | Domains section |
| User | Authenticated identity | Users section |
| Policy | Authorization rules | Domain detail view |
| Request | Authorization question | API / housectl |
| Resource | Protected thing (URI format) | In your application |
The model is designed for:
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.