# How Feature Gates Are Implemented in OpenSEO: Org-Level Permissions Explained

> Learn how OpenSEO implements feature gates using org-level permissions and declarative statements. Discover the layered permission system built on Better-Auth.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-09-09

---

**OpenSEO implements feature gates through a layered org-level permission system built on top of the Better-Auth library, using declarative permission statements in [`src/lib/org-permissions.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/org-permissions.ts) and enforcement middleware in [`src/server/auth/org-gate.ts`](https://github.com/every-app/open-seo/blob/main/src/server/auth/org-gate.ts).**

The **open-seo** repository by every-app controls access to premium functionality through a centralized **feature gate** architecture that separates permission definitions from enforcement logic. This system restricts sensitive operations like billing management and project creation to specific organizational roles while allowing members to perform research tasks.

## The Permission Catalog: Declarative Feature Statements

All feature permissions in OpenSEO originate from a single source of truth located in [`src/lib/org-permissions.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/org-permissions.ts). This file defines exactly which actions are available for each feature domain and which roles can perform them.

### Defining the Statement Object

The core of the permission system is the **statement** object that extends Better-Auth's default statements with custom feature gates. This declarative structure lists privileged actions for each feature group:

```typescript
// src/lib/org-permissions.ts
const statement = {
  ...defaultStatements,
  billing: ["manage"],          // ← feature gate for billing-related actions
  project: ["create", "delete"],
  integration: ["manage"],
} as const;

export const orgAccessControl = createAccessControl(statement);

```

The statement explicitly enumerates actions for `billing`, `project`, and `integration` domains, creating a type-safe contract that the rest of the application uses to validate access.

### Role-Based Permission Assignment

OpenSEO establishes three distinct roles—**owner**, **admin**, and **member**—by extending the default Better-Auth organization statements with the custom feature statements defined above. These roles determine which permissions from the statement object are granted to users.

In hosted deployments, the organization's **plan** dictates which roles receive the extended statements. Self-hosted deployments bypass plan checks because administrators define roles manually, though the underlying permission verification logic remains identical.

## Access Control Layer: Evaluating Permissions

The transition from declarative statements to runtime enforcement happens through the **access control** infrastructure implemented in the permissions library.

The `createAccessControl(statement)` factory function generates an `orgAccessControl` object capable of authorizing permission requests. This object exposes a `hasOrgPermission` helper that splits the comma-separated role string stored by Better-Auth and validates it against the permission catalog.

When checking access, the system parses the role string (e.g., `"owner,admin"`) and verifies that at least one assigned role possesses the required permission statement.

## Server-Side Enforcement: Gating Critical Operations

Individual server functions enforce feature gates by importing the `requireOrgPermission` middleware from [`src/server/auth/org-gate.ts`](https://github.com/every-app/open-seo/blob/main/src/server/auth/org-gate.ts). This pattern ensures that business logic executes only after authorization succeeds.

### The Permission Middleware

The `requireOrgPermission` function acts as a gatekeeper that verifies the caller's role before allowing execution to proceed:

```typescript
// src/server/auth/org-gate.ts (simplified)
import { hasOrgPermission } from "@/lib/org-permissions";

export function requireOrgPermission(
  req: Request,
  required: OrgPermissionRequest,
) {
  const role = req.headers.get("x-org-role") ?? "";
  if (!hasOrgPermission(role, required)) {
    throw new Response("Forbidden", { status: 403 });
  }
}

```

This middleware extracts the role from request headers and delegates validation to the `hasOrgPermission` function, returning a 403 Forbidden response when checks fail.

### Gating Server Functions

Concrete implementations apply this middleware to protect specific operations. For example, project creation requires the `project:create` permission:

```typescript
// src/serverFunctions/projects.ts – gating a server function
import { requireOrgPermission } from "@/server/auth/org-gate";

export async function createProject(req: Request) {
  // Only owners and admins can create projects
  requireOrgPermission(req, { project: ["create"] });
  // …implementation…
}

```

This pattern guarantees that only **owners** or **admins** can create projects, while **members** receive immediate rejection before touching the core logic.

## Client-Side Feature Gates: Controlling UI Access

While server-side gates provide security, OpenSEO also implements client-side checks to conditionally render UI elements. The client parses the role string from the authentication session to determine interface visibility:

```tsx
// src/web/src/routes/_marketing/features/project.tsx (client side)
import { useAuth } from "@/hooks/useAuth";

export default function ProjectsPage() {
  const { role } = useAuth(); // role string from auth session
  const canCreate = role.split(",")
    .some(r => r.trim() === "owner" || r.trim() === "admin");

  return (
    <>
      <h1>Projects</h1>
      {canCreate && <CreateProjectButton />}
    </>
  );
}

```

This approach prevents users from seeing action buttons they cannot use, though the server gates remain the ultimate authority for security.

## Summary

OpenSEO's feature gate implementation provides a robust, layered approach to access control:

- **Centralized permissions**: The `statement` object in [`src/lib/org-permissions.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/org-permissions.ts) defines all feature gates in one location, making it simple to add new restrictions by extending the actions list.

- **Role-based access**: Three roles (`owner`, `admin`, `member`) inherit permissions through Better-Auth's organization system, with hosted deployments adding plan-based limitations.

- **Server enforcement**: The `requireOrgPermission` middleware in [`src/server/auth/org-gate.ts`](https://github.com/every-app/open-seo/blob/main/src/server/auth/org-gate.ts) blocks unauthorized requests at the API boundary before they reach business logic.

- **Client coordination**: UI components mirror server permissions by parsing role strings from the auth session, ensuring consistent user experiences.

- **Flexibility**: Self-hosted instances can manually assign roles without plan checks, while maintaining the same permission verification flow.

## Frequently Asked Questions

### How does OpenSEO determine which features a user can access?

OpenSEO determines feature access by evaluating the user's organizational role against the permission **statement** object defined in [`src/lib/org-permissions.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/org-permissions.ts). The system splits the comma-separated role string from the request headers and checks if any assigned role possesses the required permission action (e.g., `billing:manage` or `project:create`). This evaluation occurs server-side through the `hasOrgPermission` helper before executing protected functions.

### What is the difference between owner, admin, and member roles in OpenSEO?

The **owner** and **admin** roles receive extended permission statements that include sensitive actions like managing billing, creating or deleting projects, and configuring integrations. The **member** role typically lacks these statements, restricting users to read-only research operations. In hosted deployments, the organization's subscription plan determines whether these roles receive the additional permission statements, while self-hosted deployments allow manual role configuration.

### Can feature gates be bypassed in self-hosted OpenSEO deployments?

Self-hosted deployments can bypass **plan-based** restrictions because administrators manually define which roles receive permission statements rather than having the system automatically assign them based on billing tiers. However, the underlying permission checks in [`src/server/auth/org-gate.ts`](https://github.com/every-app/open-seo/blob/main/src/server/auth/org-gate.ts) still enforce access control—users cannot perform actions unless their assigned role explicitly includes the required permission in the statement object.

### Where should I add new permissions when extending OpenSEO's feature gates?

Add new permissions to the **statement** object in [`src/lib/org-permissions.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/org-permissions.ts) by extending the relevant feature group or creating a new one (e.g., `report: ["export", "share"]`). After updating the statement, regenerate the `orgAccessControl` object and update any server functions in `src/serverFunctions/` to call `requireOrgPermission` with the new permission request. For UI consistency, also update the client-side role checks in relevant route components.