Back to flin
flin

#115 -- Custom Guards and Security Middleware

How FLIN developers create custom guards and security middleware for application-specific access control -- verified email guards, subscription checks, IP restrictions, and audit logging.

Juste A. Gnimavo (Thales) & Claude | March 26, 2026 7 min flin
EN/ FR/ ES
flinguardsmiddlewarecustomsecurity

FLIN's nine built-in guards cover the most common access control patterns: authentication, roles, rate limiting, CSRF, API keys, IP whitelists, time restrictions, resource ownership, and method filtering. But real applications have domain-specific security requirements that no framework can anticipate in advance.

A SaaS application needs to check subscription tiers. An educational platform needs to verify enrollment status. A financial application needs audit logging on every mutation. A multi-tenant system needs to ensure users only access their own tenant's data.

FLIN's guard_definition syntax and middleware system let developers create these custom security layers with the same declarative API as built-in guards.

Defining Custom Guards

The guard_definition keyword creates a new guard type that can be used anywhere the built-in guards are used:

flin// Define a custom guard
guard_definition verified_email {
    user = request.user
    if user == none || !user.emailVerified {
        return response {
            status: 403
            body: { error: "Email verification required" }
        }
    }
}

The guard definition contains the check logic. If the function returns a response, the guard fails and that response is sent to the client. If the function returns nothing (falls through), the guard passes.

Use the custom guard exactly like a built-in guard:

flinguard auth
guard verified_email

route POST {
    // Only authenticated users with verified email reach here
}

Parameterized Custom Guards

Custom guards can accept parameters for configurable behavior:

flinguard_definition subscription(required_plan) {
    user = request.user
    if user == none {
        return response {
            status: 401
            body: { error: "Authentication required" }
        }
    }

    plan_levels = { "free": 0, "starter": 1, "pro": 2, "enterprise": 3 }
    user_level = plan_levels[user.plan] || 0
    required_level = plan_levels[required_plan] || 0

    if user_level < required_level {
        return response {
            status: 403
            body: {
                error: "This feature requires the " + required_plan + " plan",
                current_plan: user.plan,
                required_plan: required_plan,
                upgrade_url: "/pricing"
            }
        }
    }
}

// Usage
guard auth
guard subscription("pro")

route POST {
    // Only pro and enterprise users
}

The error response includes context that the frontend can use to show an upgrade prompt.

Real-World Custom Guard Patterns

Enrollment Check for Education Platform

flinguard_definition enrolled(course_id_param) {
    user = request.user
    if user == none {
        return response { status: 401, body: { error: "Authentication required" } }
    }

    course_id = to_int(params[course_id_param])
    enrollment = Enrollment.where(
        user_id == user.id && course_id == course_id
    ).first

    if enrollment == none {
        return response {
            status: 403
            body: {
                error: "You are not enrolled in this course",
                course_id: course_id,
                enroll_url: "/courses/" + to_text(course_id) + "/enroll"
            }
        }
    }

    // Attach enrollment to request for downstream use
    request.enrollment = enrollment
}

// app/api/courses/[course_id]/lessons/[id].flin
guard auth
guard enrolled("course_id")

route GET {
    // request.enrollment is available
    lesson = Lesson.find(params.id)
    lesson
}

Multi-Tenant Isolation

flinguard_definition tenant_member {
    user = request.user
    if user == none {
        return response { status: 401, body: { error: "Authentication required" } }
    }

    tenant_id = to_int(params.tenant_id || headers["X-Tenant-ID"])
    membership = TenantMember.where(
        user_id == user.id && tenant_id == tenant_id
    ).first

    if membership == none {
        return response {
            status: 403
            body: { error: "You do not have access to this tenant" }
        }
    }

    request.tenant_id = tenant_id
    request.tenant_role = membership.role
}

guard_definition tenant_admin {
    if request.tenant_role != "admin" && request.tenant_role != "owner" {
        return response {
            status: 403
            body: { error: "Admin access required for this tenant" }
        }
    }
}

// app/api/tenants/[tenant_id]/settings.flin
guard auth
guard tenant_member
guard tenant_admin

route PUT {
    // Only tenant admins and owners
}

Feature Flags

flinguard_definition feature(flag_name) {
    flag = FeatureFlag.where(name == flag_name).first

    if flag == none || !flag.enabled {
        return response {
            status: 404
            body: { error: "This feature is not available" }
        }
    }

    // Check user-specific rollout
    if flag.rollout_percent < 100 {
        user = request.user
        if user != none {
            user_hash = hash_string(to_text(user.id) + flag_name) % 100
            if user_hash >= flag.rollout_percent {
                return response {
                    status: 404
                    body: { error: "This feature is not available" }
                }
            }
        }
    }
}

// app/api/beta/ai-search.flin
guard auth
guard feature("ai_search_beta")

route POST {
    // Only available when the ai_search_beta flag is enabled
    // and the user is in the rollout percentage
}

Security Middleware Patterns

While guards handle per-route access control, middleware handles cross-cutting security concerns. Custom middleware is defined in _middleware.flin files.

Audit Logging Middleware

flin// app/api/_middleware.flin

middleware {
    start = now()

    next()

    // Log after handler completes
    if request.method != "GET" && request.method != "OPTIONS" {
        save AuditLog {
            user_id: request.user_id || 0,
            method: request.method,
            path: request.path,
            ip: request.ip,
            status: response.status,
            duration_ms: (now() - start),
            user_agent: request.user_agent,
            body_summary: truncate(to_text(body), 500)
        }
    }
}

This middleware logs every mutating request (POST, PUT, DELETE, PATCH) with the user, the endpoint, the response status, and a summary of the request body. The audit trail is automatic for every API endpoint.

Request Signing Middleware

For APIs that require request signing (webhook receivers, partner APIs):

flin// app/api/webhooks/_middleware.flin

middleware {
    signature = headers["X-Signature-256"]
    if signature == "" {
        return response { status: 401, body: { error: "Missing signature" } }
    }

    expected = hmac_sha256(request.raw_body, env("WEBHOOK_SECRET"))
    if !constant_time_eq(signature, "sha256=" + expected) {
        return response { status: 401, body: { error: "Invalid signature" } }
    }

    next()
}

Geoblocking Middleware

flin// app/api/restricted/_middleware.flin

middleware {
    country = geoip_country(request.ip)

    blocked = ["XX", "YY"]  // Sanctioned countries
    if blocked.contains(country) {
        return response {
            status: 403
            body: { error: "Service not available in your region" }
        }
    }

    request.country = country
    next()
}

Guard Composition Patterns

Custom guards compose naturally with built-in guards using AND logic:

flin// Maximum security for financial operations
guard auth
guard role("accountant", "admin")
guard verified_email
guard subscription("enterprise")
guard tenant_member
guard rate_limit(10, 60)
guard time("08:00", "18:00")

route POST {
    // This endpoint requires:
    // 1. Authentication
    // 2. Accountant or admin role
    // 3. Verified email address
    // 4. Enterprise subscription
    // 5. Tenant membership
    // 6. Under rate limit
    // 7. During business hours
}

Seven guards, seven lines. Each is evaluated in order. If any fails, the request is rejected with the appropriate status code and error message.

Testing Custom Guards

Custom guards should be tested with the same rigor as built-in guards:

flin// test/guards/subscription_guard_test.flin

test "subscription guard rejects free user for pro feature" {
    ctx = mock_request_context({ user: { plan: "free" } })
    result = guard_subscription(ctx, ["pro"])
    assert result.status == 403
}

test "subscription guard accepts pro user for pro feature" {
    ctx = mock_request_context({ user: { plan: "pro" } })
    result = guard_subscription(ctx, ["pro"])
    assert result == pass
}

test "subscription guard accepts enterprise user for pro feature" {
    ctx = mock_request_context({ user: { plan: "enterprise" } })
    result = guard_subscription(ctx, ["pro"])
    assert result == pass
}

The Architecture of Extensible Security

FLIN's security architecture follows a principle: defaults are non-negotiable, extensions are unlimited.

The nine built-in guards and the automatic security headers cannot be removed or weakened. They provide a baseline that every FLIN application inherits. But above that baseline, developers can add any number of custom guards, custom middleware, and custom validation rules to implement their specific security requirements.

This architecture means: - A new FLIN developer gets enterprise-grade security out of the box. - An experienced developer can extend the security model without fighting the framework. - Security features are visible, declarative, and testable. - The attack surface is known and bounded.

This concludes Arc 10 -- FLIN's security features. Ten articles covering OWASP Top 10 coverage, Argon2 password hashing, JWT authentication, rate limiting, security headers, 2FA, OAuth2, WhatsApp OTP, input validation, security testing, and custom guards. In Arc 11, we enter the most innovative part of FLIN: the AI and Intent Engine, where natural language meets database queries.


This is Part 115 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.

Series Navigation: - [114] 75 Security Tests: How We Verified Everything - [115] Custom Guards and Security Middleware (you are here) - [116] The Intent Engine: Natural Language Database Queries - [117] Semantic Search and Vector Storage

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude thales

Thirteen Agents, Forty-Three Minutes: The First Claude Fable 5 Workflow Session, And What A Deterministic Orchestration Script Changes About Multi-Agent Builds

One prompt, thirteen agents, forty-three minutes: the first production session with Claude Fable 5 and Claude Code's Workflow tool shipped a complete seven-page production website plus a backend lead-capture endpoint in a single commit. The build log: the deterministic orchestration script, the contract-injection pattern between phases, the per-agent economics of the parallel fan-out, and the session-limit cliffhanger the resume journal turned into a non-event.

20 min Jun 12, 2026
claude-fable-5claude-codeworkflow-toolmulti-agent +10
Thales & Claude casp

The gate caught its own drift: one day inside CASP with Claude Fable 5

We handed the most autonomous Claude model yet the keys to CASP — the open-source CLI that keeps AI coding agents honest against git — with the authority to reject our own roadmap. It rejected five things, found two real bugs in the validator by dogfooding it, fixed them under a two-auditor gate, and left casp check fully green on its own repo for the first time. CASP 0.3.0 is the result.

14 min Jun 10, 2026
caspzerosuiteworkflowai-cto +9
Thales & Claude zerosuite

The CASP Transplant: How The Six-File Discipline Moved From Conductor To An Anti-Fraud Transport ERP, What The /next Skill Adds When The Operator Just Types 'next', And Why The Cost Of CASP Drift Rises When The Project Is Someone Else's Cash

The CASP discipline that ran thirty-five Conductor sessions is product-agnostic. The build log of transplanting it to KASSIA, an anti-fraud transport ERP for a Côte d'Ivoire fleet operator: what moved, what did not (the bespoke validator — and what its absence costs), what the /next skill adds when the operator types one word, and where the CASP stops — the deployment bug it could not see because it records intent, not infrastructure reality.

20 min Jun 8, 2026
kassiaerp-kassia-transport-logistiquezerosuiteCASP +15