How Feature Gates Are Implemented in OpenSEO: Org-Level Permissions Explained
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 and enforcement middleware in 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. 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:
// 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. 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:
// 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:
// 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:
// 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
statementobject insrc/lib/org-permissions.tsdefines 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
requireOrgPermissionmiddleware insrc/server/auth/org-gate.tsblocks 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. 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 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →