Skip to main content

Design Philosophy

cedar4s is designed around several core principles that guide its architecture and API design.

Inspiration from smithy4s​

cedar4s is heavily inspired by smithy4s, which generates type-safe Scala code from Smithy API definitions. We apply the same approach to Cedar authorization schemas:

smithy4scedar4s
Smithy API specsCedar schemas
Generated service traitsGenerated action DSL
Type-safe request/responseType-safe auth requests
Protocol-agnosticEffect-polymorphic

The key insight: authorization models, like API definitions, benefit from code generation that provides compile-time guarantees.

Type Safety at Compile Time​

Authorization errors should be caught at compile time, not runtime:

import myapp.cedar.MyApp.*

// Compile error: DocumentId expected, got FolderId
Document.Read(folderId, folderId)

// Compile error: User cannot perform Admin action (no CanPerform evidence)
AdminAction.DeleteAll(userId).asPrincipal(Principals.User(userId))

// Compile error: Missing required EntityFetcher registration
val store = EntityStore.builder[Future]()
// .register[Entities.Document, String](docFetcher) // Forgot this!
.build()
// Error when Document.Read.on(id) is called

Separation of Concerns​

cedar4s separates authorization into distinct layers:

Each layer has a single responsibility:

  • DSL: Express what permission is needed
  • CedarSession: Orchestrate the authorization check
  • EntityStore: Load entity data
  • Cedar Engine: Evaluate policies

Effect Polymorphism​

cedar4s doesn't force a specific effect type. All APIs are generic over F[_]:

trait CedarSession[F[_]]
trait EntityStore[F[_]]
trait EntityFetcher[F[_], E, Id]

This means:

  • Use Future for simple applications
  • Use cats-effect IO for functional applications
  • Use ZIO for ZIO-based applications
  • Use custom effects for specialized needs

We provide minimal type classes (Sync, Concurrent, FlatMap) to avoid forcing external dependencies.

Composability​

Authorization checks compose naturally:

import myapp.cedar.MyApp.*

// Single check
val single = Document.Read(folderId, docId)

// Combine with AND
val both = Folder.Read(folderId) & Document.Read(folderId, docId)

// Combine with OR
val either = Document.Edit(folderId, docId) | Document.Admin(folderId, docId)

// Complex composition
val complex = (Folder.Read(folderId) & Document.Read(folderId, docId)) | Admin.Override(userId)

Composition is:

  • Type-safe: Compiler tracks what's being combined
  • Lazy: Nothing executes until .run, .require, or .isAllowed
  • Optimizable: Batch operations can optimize composed checks

Minimal Dependencies​

cedar4s core depends only on:

  • Scala standard library
  • cedar-java (Cedar policy engine)

Optional modules add specific capabilities:

  • cedar4s-caffeine: Caffeine-based caching

For other effect libraries (cats-effect, ZIO), you provide bridge instances that adapt cedar4s type classes to your effect type. See Effect Types for examples.

Schema as Source of Truth​

The Cedar schema is the single source of truth for your authorization model:

namespace MyApp {
entity User {}
entity Folder {}
entity Document in [Folder] {
owner: User,
locked: Bool
}

action "Document::Read" appliesTo {
principal: [User],
resource: Document
};
}

From this schema, cedar4s generates:

  • Entity types (Entities.Document)
  • Action types (Actions.Document.Read)
  • DSL methods (Document.Read(folderId, docId))
  • Type evidence (CanPerform[User, Document.Read])

Changes to the schema propagate to all generated code, ensuring consistency.

Explicit Over Implicit​

cedar4s favors explicit configuration over magic:

// Explicit registration of fetchers
val store = EntityStore.builder[Future]()
.register[Entities.User, String](userFetcher)
.register[Entities.Document, String](documentFetcher)
.build()

// Explicit session creation at boundary
val runtime = CedarRuntime[Future](engine, store, CedarRuntime.resolverFrom(buildPrincipal))
val session = runtime.session(Principals.User(currentUserId))

// Explicit cache configuration
val cache = CaffeineEntityCache[Future](CaffeineCacheConfig(
maximumSize = 10_000,
expireAfterWrite = Some(5.minutes)
))