Skip to content

Security & Permissions (requires) ​

In standard web development, authentication and authorization are often the messiest parts of the codebase. A developer might remember to put JWT middleware on a route, but forget to check if the user is an "Admin" inside the actual function.

If you let an AI write this logic from scratch, the chances of it hallucinating a bypass or forgetting a role check are unacceptably high.

Carotene removes security logic from the implementation entirely. You define Identities at the architectural level, and you enforce them using the requires verb.

Defining an Identity ​

Just as a model defines the shape of your data, an identity defines the shape of your authenticated user session (the payload of your JWT or session cookie).

You define this in the global scope so the compiler knows exactly what data is guaranteed to exist when a user is logged in.

dart
domain Security {
  
  // Defines the baseline authenticated user
  identity AuthenticatedUser {
    id: UUID
    email: String
    role: Enum(User, Admin) { default: User }
  }
}

The Ingress Shield: The requires Verb ​

To lock down a function or loop, you do not write if (!req.headers.authorization) return 401. You simply add the requiresverb to your permission matrix.

dart
backend {
  function UpdateProfile(userId: store.User.id, payload: UpdateProfileDTO) {
    // 1. The Security Guardrail
    requires AuthenticatedUser
    
    // 2. The Data Guardrails
    reads store.User
    updates store.User
  }
}

What the Compiler Generates ​

When the Carotene compiler sees requires AuthenticatedUser, it instantly generates an impenetrable Ingress Shield:

  1. Network Middleware: It generates the API gateway middleware that intercepts the request, verifies the session token, and extracts the identity payload.
  2. Instant Rejection: If the token is missing, expired, or invalid, the request is rejected with a 401 Unauthorized before the AI's business logic is ever touched.
  3. Sandbox Injection: If the token is valid, the verified identity is injected directly into the AI's execution context (ctx.auth.id). The AI does not decode tokens; it simply uses the verified data.

Role-Based Access Control (RBAC) ​

Checking if someone is logged in is easy; checking if they have the right to perform an action is where most security flaws happen. Carotene allows you to apply strict constraints directly to the requires verb to enforce Role-Based Access Control.

You can enforce these constraints inline using a block:

dart
backend {
  function DeleteSystemUser(targetId: store.User.id) {
    // Rejects the request with a 403 Forbidden unless the role is explicitly 'Admin'
    requires AuthenticatedUser { role: Admin }
    
    reads store.User
    deletes store.User
  }
}

By putting the RBAC check in the architectural blueprint, the AI physically cannot implement DeleteSystemUser in a way that allows standard users to trigger it.

The Frontend UI Guard (Unified Gatekeepers) ​

Carotene's greatest superpower is how the backend security rules bleed perfectly into the frontend UI via the auto-generated SDK.

1. Visibility (isAuthorized) ​

The SDK generates a standalone .isAuthorized() method. This is used by the frontend (and the AI UI generator) to completely hide components from users who lack the required roles.

tex
// Standard users will not even see this section in the DOM
{backend.DeleteSystemUser.isAuthorized() && (
  <AdminPanel />
)}

(Note: The Carotene Frontend SDK automatically binds the current user's session context to these methods, so you don't have to manually pass the user object into every check).

2. The Master Toggle (canExecute) ​

You do not need to chain security checks with state machine checks. The .canExecute() method acts as the Master Gatekeeper. Under the hood, it automatically evaluates the Precondition Inference (state machines), Zod schema validation, AND the .isAuthorized() rules.

If a function cannot run for any architectural reason, .canExecute() returns false.

tex
<button 
  // Instantly disables if the user isn't an Admin, OR if the state is invalid, OR if loading
  disabled={!backend.DeleteSystemUser.canExecute(targetUser.status) || backend.DeleteSystemUser.isLoading}
  onClick={() => backend.DeleteSystemUser(targetUser.id)}
>
  Delete User
</button>

By unifying all architectural constraints into a single method, developers can build bulletproof, perfectly reactive interfaces with zero boilerplate.