No description
  • C# 98.3%
  • Shell 1.6%
  • Dockerfile 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
ferociousbyte e5d7a032da
All checks were successful
/ build-and-pack (push) Successful in 1m33s
/ push (push) Successful in 40s
/ test (push) Successful in 53s
/ build (push) Successful in 1m26s
Normalize namespace casing in unilayer.Client and unilayer.GraphQL for consistency across projects.
2026-09-30 13:12:04 +02:00
.forgejo/workflows Add unit test steps to workflows for unilayer.Models, unilayer.Client, and unilayer.yaml 2026-09-30 12:18:40 +02:00
docs Remove deprecated access control services and test data models 2026-09-30 12:16:17 +02:00
scripts Enhance: Document cluster mode and add a cluster certificate generator 2026-09-25 23:03:59 +02:00
src Normalize namespace casing in unilayer.Client and unilayer.GraphQL for consistency across projects. 2026-09-30 13:12:04 +02:00
tests Normalize namespace casing in unilayer.Client and unilayer.GraphQL for consistency across projects. 2026-09-30 13:12:04 +02:00
.dockerignore Updated .dockerignore to exclude appsettings.json and removed redundant ignore rules for cleaner configuration. 2026-07-27 15:17:32 +02:00
.gitignore Initialized unilayer project with base structure, configurations, JWT authentication, and dependencies. 2026-07-21 18:44:53 +02:00
Architecture.md Add WebP support to FileType enum and related documentation 2026-09-26 11:58:35 +02:00
CLA.md Add licensing files and contribution guidelines 2026-09-29 22:47:22 +02:00
CONTRIBUTING.md Add licensing files and contribution guidelines 2026-09-29 22:47:22 +02:00
Dockerfile Fix: Make cluster failover stable when a peer black-holes, restores atomic, and certs observable 2026-09-26 00:16:51 +02:00
LICENSE Add licensing files and contribution guidelines 2026-09-29 22:47:22 +02:00
README.md Add licensing files and contribution guidelines 2026-09-29 22:47:22 +02:00
unilayer.slnx Remove deprecated access control services and test data models 2026-09-30 12:16:17 +02:00

unilayer

A modular REST API interface designed to provide secure, scalable, and maintainable access to Collections (database tables), Assets (files), and Users (user and permission management). Acts as an intermediary layer between client applications and backend systems (database, storage, cache).

Features

  • Dynamic Collections: Create and manage database tables at runtime via REST API
  • Asset Management: Upload, store, and retrieve files with configurable storage backends
  • User Management: Role-based access control with JWT/bearer token authentication
  • Permission System: String-based permissions with policy and role management via REST API
  • Caching: Built-in Valkey support for performance optimization
  • Multi-backend Support: PostgreSQL databases, Local/S3 (MinIO) storage, Valkey cache

Documentation

Requirements

  • .NET 10.0 SDK
  • PostgreSQL 14+
  • Valkey (Redis-compatible) 7.0+
  • Optional: MinIO or S3-compatible storage for cloud storage

Installation

PostgreSQL

macOS (Homebrew):

brew install postgresql@16
brew services start postgresql@16

Ubuntu/Debian:

sudo apt update
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql
sudo systemctl enable postgresql

Windows (Chocolatey):

choco install postgresql16

Create a database and user:

CREATE DATABASE unilayer;
CREATE USER unilayer_user WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE unilayer TO unilayer_user;

Valkey

macOS (Homebrew):

brew install valkey
brew services start valkey

Ubuntu/Debian:

sudo apt update
sudo apt install valkey-server
sudo systemctl start valkey-server
sudo systemctl enable valkey-server

Windows: Download from Valkey releases and run as service.

Docker:

docker run -d --name valkey -p 6379:6379 -v valkey-data:/data valkey/valkey

MinIO (Optional - for S3 storage)

Docker:

docker run -d -p 9000:9000 -p 9001:9001 minio/minio server /data --console-address ":9001"

Access the MinIO console at http://localhost:9001 (default credentials: minioadmin/minioadmin).

Configuration

Create an appsettings.yaml file in the project root:

# Server configuration
endpoint: "0.0.0.0:5380"

# Authentication
authentication:
  staticApiToken: "your-secure-api-token"

# Database configuration (PostgreSQL example)
database:
  providerType: "PostgreSQL"
  connectionString: "Host=localhost;Database=unilayer;Username=unilayer_user;Password=your_secure_password;Port=5432"

# Valkey configuration
valkey:
  host: "localhost"
  port: 6379
  password: ""
  db: 0

# Storage configuration (Local filesystem)
storage:
  provider: "local"

local:
  path: "./storage/assets"

# Alternative: MinIO/S3 storage
# storage:
#   provider: "minio"
# s3:
#   endpoint: "http://localhost:9000"
#   accessKey: "minioadmin"
#   secretKey: "minioadmin"
#   bucketName: "unilayer-assets"
#   useSsl: false
#   createBucketIfNotExists: true

Configuration Options:

Component Config Path Description
Database database:providerType PostgreSQL
Database database:connectionString Connection string for the database
Valkey valkey:host Valkey server hostname
Valkey valkey:port Valkey server port
Valkey valkey:password Valkey authentication password
Storage storage:provider local or minio
Local Storage local:path Base directory for file storage
MinIO/S3 s3:endpoint MinIO/S3 endpoint URL
MinIO/S3 s3:accessKey Access key for authentication
MinIO/S3 s3:secretKey Secret key for authentication
MinIO/S3 s3:bucketName Bucket name for storage

Quick Start

  1. Install dependencies (PostgreSQL, Valkey)
  2. Create appsettings.yaml with your configuration
  3. Run database migrations (automatic on startup)
  4. Start the API:
dotnet run --project src/unilayer

The API will be available at http://localhost:5380 with Swagger UI at http://localhost:5380/swagger.

Docker

Build and run with Docker:

docker build -t unilayer .
docker run -d -p 5380:5380 --name unilayer unilayer

API Overview

Base URL: /api/v1

Endpoint Method Description Required Permission
/collections GET List all collections collections:read
/collections/{name} GET Get all items in a collection collections:read or collections:{scope}:read
/collections/{name}/info GET Get collection metadata and fields collections:read
/collections/{name} POST Create a new collection collections:write
/assets GET List all assets assets:read
/assets/{uid} GET Download an asset (optional image modifiers) assets:read
/assets POST Upload an asset assets:write
/users GET List all users users:read
/users POST Create a new user users:write
/users/{id} GET Get a specific user users:read
/users/{id} DELETE Delete a user users:write
/users/{id}/role PUT Update a user's role users:write
/roles GET List all roles security:read
/roles POST Create a new role security:write
/roles/{name} GET Get a specific role security:read
/roles/{roleId} PUT Update a role security:write
/roles/{roleId} DELETE Delete a role security:write
/roles/{roleId}/policies/{policyId} POST Assign policy to role security:write
/roles/{roleId}/policies/{policyId} DELETE Remove policy from role security:write
/policies GET List all policies security:read
/policies POST Create a new policy security:write
/policies/{name} GET Get a specific policy security:read
/policies/{policyId} PUT Update a policy security:write
/policies/{policyId} DELETE Delete a policy security:write
/policies/{policyId}/permissions/{permission} POST Add permission to policy security:write
/policies/{policyId}/permissions/{permission} DELETE Remove permission from policy security:write

See the full API documentation for detailed endpoint specifications.

Query Parameters

The collections list endpoint supports the following query parameters:

Parameter Type Description
limit integer Maximum number of items to return (default: 100)
offset integer Number of items to skip (default: 0)
fields string Comma-separated list of fields to include
sort string Sort order for results
filter string Filter expression

Sort Parameter:

  • No sort: Orders by unilayer_index ASC (insertion order)
  • sort=asc: Orders by unilayer_index ASC
  • sort=desc: Orders by unilayer_index DESC
  • sort=fieldname: Orders by fieldname ASC
  • sort=fieldname:asc: Orders by fieldname ASC
  • sort=fieldname:desc: Orders by fieldname DESC
  • sort=field1,field2:desc: Orders by field1 ASC, field2 DESC

Response Format

All collection item responses are wrapped with system metadata separated from user data to prevent field name collisions.

Single Item Response

{
  "data": {
    "name": "Test Item",
    "description": "A test item"
  },
  "metadata": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "index": 1,
    "createdAt": "2024-01-01T10:00:00Z",
    "updatedAt": null
  }
}

Multiple Items Response

{
  "data": [
    {"name": "Item 1", "description": "First item"},
    {"name": "Item 2", "description": "Second item"}
  ],
  "metadata": {
    "count": 2,
    "limit": 100,
    "offset": 0,
    "has_more": false
  }
}

Collection Info Response

{
  "name": "mycollection",
  "tableName": "unilayer_mycollection",
  "createdAt": "2024-01-01T10:00:00Z",
  "updatedAt": null,
  "isSystem": false,
  "fields": [
    {"name": "name", "type": "TEXT", "nullable": false},
    {"name": "description", "type": "TEXT", "nullable": true}
  ],
  "systemFields": [
    {"name": "unilayer_id", "columnName": "unilayer_id", "type": "UUID", "isPrimaryKey": true, "isNullable": false},
    {"name": "unilayer_index", "columnName": "unilayer_index", "type": "BIGSERIAL", "isPrimaryKey": false, "isNullable": false},
    {"name": "unilayer_created_at", "columnName": "unilayer_created_at", "type": "TIMESTAMP", "isPrimaryKey": false, "isNullable": false},
    {"name": "unilayer_updated_at", "columnName": "unilayer_updated_at", "type": "TIMESTAMP", "isPrimaryKey": false, "isNullable": true}
  ]
}

Note: System fields (unilayer_id, unilayer_index, unilayer_created_at, unilayer_updated_at) are automatically managed by unilayer and cannot be defined by users. Field names starting with unilayer_ are reserved.

Authentication

Authentication uses Bearer tokens. Include the token in the Authorization header:

Authorization: Bearer your-secure-api-token

Permission System

unilayer uses a string-based permission system with roles and policies:

  • Policies contain a list of permission strings (e.g., collections:read, assets:write)
  • Roles are assigned one or more policies
  • Users are assigned a single role
  • Write permissions automatically imply read permissions (e.g., collections:write includes collections:read)

Permission String Format

Permission Description
collections:read Read all collections
collections:write Create/update/delete all collections
collections:{scope}:read Read collections with a specific scope prefix
collections:{scope}:write Write collections with a specific scope prefix
collections:{scope}:{name}:read Read a specific collection in a scope
collections:{scope}:{name}:write Write a specific collection in a scope
assets:read Read all assets
assets:write Create/update/delete all assets
users:read Read all users
users:write Create/update/delete all users
security:read Read all roles and policies
security:write Create/modify/delete roles and policies

Built-in Immutable Entities

On first startup, unilayer automatically creates:

  1. fullaccess policy - Contains all permissions (collections:read, collections:write, assets:read, assets:write, users:read, users:write, security:read, security:write)
  2. admin role - Assigned the fullaccess policy, immutable
  3. root user - Created from the static API token in configuration, assigned the admin role

Note: The static API token from configuration is used only once to create the root user. After that, the hashed token is stored in the database and the static token is no longer needed.

Default Roles

  • admin - Full access to all system resources (immutable)
  • user - Default role for new users (configurable)

API Examples

Authentication

All requests require a valid Bearer token:

# Using the root user token (from initial configuration)
ROOT_TOKEN="your-initial-static-token"

# Or use a token from a created user
USER_TOKEN="user-specific-token"

curl -H "Authorization: Bearer $ROOT_TOKEN" \
  http://localhost:5380/api/v1/users

Creating a New User

curl -X POST \
  -H "Authorization: Bearer $ROOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username": "john", "role": "user"}' \
  http://localhost:5380/api/v1/users

Response:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "username": "john",
  "role": "user",
  "apiToken": "generated-token-for-john",
  "createdAt": "2026-08-30T20:00:00Z"
}

Creating a New Policy

curl -X POST \
  -H "Authorization: Bearer $ROOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "read-only",
    "description": "Read-only access to collections and assets",
    "permissions": [
      "collections:read",
      "assets:read"
    ]
  }' \
  http://localhost:5380/api/v1/policies

Response:

{
  "id": "12345678-1234-5678-1234-567812345678",
  "name": "read-only",
  "description": "Read-only access to collections and assets",
  "createdAt": "2026-08-30T20:00:00Z",
  "isImmutable": false,
  "permissions": ["collections:read", "assets:read"]
}

Creating a New Role

curl -X POST \
  -H "Authorization: Bearer $ROOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "viewer",
    "description": "View-only role"
  }' \
  http://localhost:5380/api/v1/roles

Response:

{
  "id": "87654321-4321-8765-4321-876543218765",
  "name": "viewer",
  "description": "View-only role",
  "createdAt": "2026-08-30T20:00:00Z",
  "isImmutable": false,
  "policies": []
}

Assigning a Policy to a Role

# Get the policy ID and role ID first
POLICY_ID="12345678-1234-5678-1234-567812345678"
ROLE_ID="87654321-4321-8765-4321-876543218765"

curl -X POST \
  -H "Authorization: Bearer $ROOT_TOKEN" \
  http://localhost:5380/api/v1/roles/$ROLE_ID/policies/$POLICY_ID

Adding a Permission to a Policy

POLICY_ID="12345678-1234-5678-1234-567812345678"

curl -X POST \
  -H "Authorization: Bearer $ROOT_TOKEN" \
  http://localhost:5380/api/v1/policies/$POLICY_ID/permissions/users:read

Listing All Roles

curl -H "Authorization: Bearer $ROOT_TOKEN" \
  http://localhost:5380/api/v1/roles

Response:

[
  {
    "id": "00000000-0000-0000-0000-000000000002",
    "name": "admin",
    "description": "System administrator with full access. This role is immutable.",
    "createdAt": "2026-08-30T19:52:57Z",
    "isImmutable": true,
    "policies": ["fullaccess"]
  },
  {
    "id": "87654321-4321-8765-4321-876543218765",
    "name": "viewer",
    "description": "View-only role",
    "createdAt": "2026-08-30T20:00:00Z",
    "isImmutable": false,
    "policies": ["read-only"]
  }
]

Listing All Policies

curl -H "Authorization: Bearer $ROOT_TOKEN" \
  http://localhost:5380/api/v1/policies

Response:

[
  {
    "id": "00000000-0000-0000-0000-000000000001",
    "name": "fullaccess",
    "description": "Full access to all system resources, collections, assets, users, and security settings",
    "createdAt": "2026-08-30T19:52:57Z",
    "isImmutable": true,
    "permissions": [
      "collections:read",
      "collections:write",
      "assets:read",
      "assets:write",
      "users:read",
      "users:write",
      "security:read",
      "security:write"
    ]
  }
]

Getting Current User's Permissions

curl -H "Authorization: Bearer $ROOT_TOKEN" \
  http://localhost:5380/api/v1/users/me/permissions

Response:

[
  "collections:read",
  "collections:write",
  "assets:read",
  "assets:write",
  "users:read",
  "users:write",
  "security:read",
  "security:write"
]

Supported Backends

Component Supported Backends
Database PostgreSQL
Storage Local Filesystem, MinIO/S3
Cache Valkey

License

unilayer is open source. The repository uses two licenses:

Component Path License
Server (API, cluster, GraphQL, schema) everything not listed below AGPL-3.0-only
.NET client (Unilayer.Client) src/unilayer.Client/ MIT
Shared models (Unilayer.Models) src/unilayer.Models/ MIT

A directory with its own LICENSE file is covered by that license; everything else is covered by the root LICENSE.

What this means in practice:

  • Self-hosting unilayer is free, including for commercial use.
  • The client and models can be used in any application, including closed-source and commercial ones. Using the client does not put your application under the AGPL.
  • If you modify the server and let others use it over a network, e.g. by offering it as a hosted service, you must make your modified source code available to those users under the AGPL.

Commercial support is available from Talaryon Labs.

Copyright (c) 2026 Talaryon Labs.

Contributing

Contributions are welcome. See CONTRIBUTING.md.