- C# 98.3%
- Shell 1.6%
- Dockerfile 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| docs | ||
| scripts | ||
| src | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| Architecture.md | ||
| CLA.md | ||
| CONTRIBUTING.md | ||
| Dockerfile | ||
| LICENSE | ||
| README.md | ||
| unilayer.slnx | ||
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
- Architecture Overview
- API Documentation
- Collections - Dynamic table management
- Assets - File storage and retrieval
- Users - User and permission management
- Permissions - Permission system and policy management
- Filters - Query filtering and sorting
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
- Install dependencies (PostgreSQL, Valkey)
- Create
appsettings.yamlwith your configuration - Run database migrations (automatic on startup)
- 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_indexASC (insertion order) sort=asc: Orders byunilayer_indexASCsort=desc: Orders byunilayer_indexDESCsort=fieldname: Orders byfieldnameASCsort=fieldname:asc: Orders byfieldnameASCsort=fieldname:desc: Orders byfieldnameDESCsort=field1,field2:desc: Orders byfield1ASC,field2DESC
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 withunilayer_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:writeincludescollections: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:
fullaccesspolicy - Contains all permissions (collections:read,collections:write,assets:read,assets:write,users:read,users:write,security:read,security:write)adminrole - Assigned thefullaccesspolicy, immutablerootuser - Created from the static API token in configuration, assigned theadminrole
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.