SkillAgentSearch skills...

Liminal

A collection of http Motoko libraries. Routing, parsing, middleware, etc...

Install / Use

/learn @edjCase/Liminal
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Liminal

Logo

MOPS License

A middleware-based HTTP framework for Motoko on the Internet Computer.

Overview

Liminal is a flexible HTTP framework designed to make building web applications with Motoko simpler and more maintainable. It provides a middleware pipeline architecture, built-in routing capabilities, and a variety of helper modules for common web development tasks.

Key features:

  • 🔄 Middleware: Compose your application using reusable middleware components
  • 🛣️ Routing: Powerful route matching with parameter extraction and group support
  • 🔒 CORS Support: Configurable Cross-Origin Resource Sharing
  • 🔐 CSP Support: Content Security Policy configuration
  • 📦 Asset Canister Integration: Simplified interface with Internet Computer's certified assets
  • 🔑 JWT Authentication: Built-in JWT parsing and validation
  • 🚀 Compression: Automatic response compression for performance
  • ⏱️ Rate Limiting: Protect your APIs from abuse
  • 🛡️ Authentication: Configurable authentication requirements
  • 🔀 Content Negotiation: Automatically convert data to JSON, CBOR, XML based on Accept header
  • 📤 File Uploads: Parse and process multipart/form-data for handling file uploads (limited to 2MB)
  • 📝 Logging: Built-in logging system with configurable levels and custom logger support
  • 🔐 OAuth Authentication: Built-in OAuth 2.0 support with PKCE for Google, GitHub, and custom providers

Package

MOPS

mops add liminal

To setup MOPS package manager, follow the instructions from the MOPS Site

Liminal Middleware Pipeline

Liminal uses a middleware pipeline pattern where each middleware component processes requests as they flow down the pipeline, and then processes responses as they flow back up in reverse order. This creates a "sandwich" effect where the first middleware to see a request is the last to process the response.

Basic Flow Example

                  Request ──┐     ┌─> Response
                            │     |
                            ▼     │
                        ┌─────────────┐
        - Decompresses  │ Compression │ - Compresses
          request       │ Middleware  │   response
                        └─────────────┘
                            │     ▲
                            ▼     │
                        ┌─────────────┐
        - Parses JWT    │  JWT        │ - Ignores
        - Sets identity │  Middleware │   response
                        └─────────────┘
                            │     ▲
                            ▼     │
                        ┌─────────────┐
        - Matches url   │ API Router  │ - Returns API
          to function   │ Middleware  │   response
                        └─────────────┘

How It Works

  1. Request Flow (Down): The HTTP request starts at the first middleware and flows down through each middleware in the order they were defined in your middleware array.

  2. Response Generation: Any middleware in the pipeline can choose to generate a response and stop the request flow. When this happens, the response immediately begins flowing back up through only the middleware that have already processed the request, bypassing any remaining middleware further down the pipeline.

  3. Response Flow (Up): The response then flows back up through the middleware pipeline in reverse order, allowing each middleware to modify or enhance the response.

Query/Update Upgrade Flow

How Middleware Handles Query→Update Upgrades

In the Internet Computer, all HTTP requests start as Query calls (fast, read-only). If a middleware needs to modify state or make async calls, it can upgrade the request to an Update call (slower, can modify state). When this happens, the entire request restarts from the beginning with the same middleware pipeline.

Upgrade Flow Example

        Query Flow                       Update Flow


Request ──┐             ┌──────► Request ──┐     ┌─► Response
          │             │                  │     │
          ▼             │                  ▼     │
      ┌─────────────┐   │              ┌─────────────┐
      │ Compression │   │              │ Compression │
      │ Middleware  │   │              │ Middleware  │
      └─────────────┘   │              └─────────────┘
          │             │                  │     ▲
          ▼             │                  ▼     │
      ┌─────────────┐   │              ┌─────────────┐
      │ JWT         │ ──┘              │ JWT         │
      │ Middleware  │ Upgrade          │ Middleware  │
      └─────────────┘                  └─────────────┘
                                           │     ▲
                                           ▼     │
      ┌─────────────┐                  ┌─────────────┐
      │ API Router  │                  │ API Router  │
      │ Middleware  │                  │ Middleware  │
      └─────────────┘                  └─────────────┘

How Upgrades Work

  1. Query Processing: Request flows down through middleware as Query calls (fast path)

  2. Upgrade Decision: Any middleware can decide it needs to upgrade (e.g., needs to modify state, make async calls)

  3. Request Restart: When upgraded, the entire request restarts from the beginning as an Update call and go through each middleware again

Quick Start

Here's a minimal example to get started:

import Liminal "mo:liminal";
import Route "mo:liminal/Route";
import Router "mo:liminal/Router";
import RouteContext "mo:liminal/RouteContext";
import RouterMiddleware "mo:liminal/Middleware/Router";
import CORSMiddleware "mo:liminal/Middleware/CORS";

actor {
    // Define your routes
    let routerConfig = {
        prefix = ?"/api";
        identityRequirement = null;
        routes = [
            Router.get(
                "/hello/{name}",
                #query_(func(context : RouteContext.RouteContext) : Route.HttpResponse {
                    let name = context.getRouteParam("name");
                    context.buildResponse(#ok, #text("Hello, " # name # "!"));
                })
            )
        ]
    };

    // Create the HTTP App with middleware
    let app = Liminal.App({
        middleware = [
            // Order matters
            // First middleware will be called FIRST with the HTTP request
            // and LAST with handling the HTTP response
            CORSMiddleware.default(),
            RouterMiddleware.new(routerConfig),
        ];
        errorSerializer = Liminal.defaultJsonErrorSerializer;
        candidRepresentationNegotiator = Liminal.defaultCandidRepresentationNegotiator;
        logger = Liminal.buildDebugLogger(#info);
        urlNormalization = {
            pathIsCaseSensitive = false;
            preserveTrailingSlash = false;
            queryKeysAreCaseSensitive = false;
            removeEmptyPathSegments = true;
            resolvePathDotSegments = true;
            usernameIsCaseSensitive = false;
        };
    });

    // Expose standard HTTP interface
    public query func http_request(request : Liminal.RawQueryHttpRequest) : async Liminal.RawQueryHttpResponse {
        app.http_request(request)
    };

    public func http_request_update(request : Liminal.RawUpdateHttpRequest) : async Liminal.RawUpdateHttpResponse {
        await* app.http_request_update(request)
    };
}

More Complete Example

Here's a more comprehensive example demonstrating multiple middleware components:

import Liminal "mo:liminal";
import Route "mo:liminal/Route";
import Router "mo:liminal/Router";
import RouteContext "mo:liminal/RouteContext";
import RouterMiddleware "mo:liminal/Middleware/Router";
import CORSMiddleware "mo:liminal/Middleware/CORS";
import JWTMiddleware "mo:liminal/Middleware/JWT";
import CompressionMiddleware "mo:liminal/Middleware/Compression";
import CSPMiddleware "mo:liminal/Middleware/CSP";
import AssetsMiddleware "mo:liminal/Middleware/Assets";
import SessionMiddleware "mo:liminal/Middleware/Session";
import HttpAssets "mo:http-assets";

actor {
    // Define your routes
    let routerConfig = {
        prefix = ?"/api";
        identityRequirement = null;
        routes = [
            Router.get(
                "/public",
                #query_(func(context : RouteContext.RouteContext) : Route.HttpResponse {
                    context.buildResponse(#ok, #text("Public endpoint"))
                })
            ),
            Router.groupWithAuthorization(
                "/secure",
                [
                    Router.get(
                        "/profile",
                        #query_(func(context : RouteContext.RouteContext) : Route.HttpResponse {
                            context.buildResponse(#ok, #text("Secure profile endpoint"))
                        })
                    )
                ],
                #authenticated
            )
        ]
    };

    ];

    // Initialize asset store
    let canisterId = Principal.fromActor(self);
    let assetStableData = HttpAssets.init_stable_store(canisterId, initializer)
    |> HttpAssets.upgrade_stable_store(_);
    let assetStore = HttpAssets.Assets(assetStableData, ?setPermissions);

    // Create the HTTP App with middleware
    let app = Liminal.App({
        middleware = [
            // Order matters - middleware are executed in this order for requests
            // and in reverse order for responses
            CompressionMiddleware.default(),
            CORSMiddleware.default(),
            SessionMiddleware.inMemoryDefault(),
            JWTMiddleware.new({
                locations = JWTMiddleware.defaultLocations;
                validation = {
                    audience = #skip;
  

Related Skills

View on GitHub
GitHub Stars11
CategoryDevelopment
Updated5mo ago
Forks0

Languages

Motoko

Security Score

87/100

Audited on Oct 31, 2025

No findings