- SQLite: table recreation (doesn't support ALTER COLUMN) - PostgreSQL: ALTER COLUMN DROP NOT NULL - MySQL: MODIFY COLUMN - Handle fresh database (create table directly) - Handle partial migration failure (drop temp table first) |
||
|---|---|---|
| arch | ||
| debian | ||
| migration | ||
| plugins | ||
| src | ||
| static/data | ||
| storage/themes | ||
| templates | ||
| themes | ||
| types | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .luarc.json | ||
| ARCHITECTURE.md | ||
| build | ||
| build.rs | ||
| cargo | ||
| Cargo.lock | ||
| Cargo.toml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Makefile | ||
| README.md | ||
| TODO.md | ||
Amity v2
A personal cloud platform with plugin support.
Building
cargo build --release
Running the Server
# Basic usage (uses default port 3000)
AMITY_DATABASE_URL="sqlite:./amity.db?mode=rwc" ./target/release/amity serve
# Custom port
AMITY_DATABASE_URL="sqlite:./amity.db?mode=rwc" ./target/release/amity serve --port 9090
# With custom bind address
AMITY_DATABASE_URL="sqlite:./amity.db?mode=rwc" ./target/release/amity serve --bind 127.0.0.1 --port 9090
Plugin System
Plugin Structure
Plugins are distributed as .tar.gz archives containing:
plugin-name/
├── manifest.toml # Plugin metadata (required)
├── signature.txt # Ed25519 signature (required)
├── init.lua # Entry point (required)
├── templates/ # Tera templates (optional)
│ └── *.html
├── static/ # Static assets (optional)
│ └── css/
│ └── *.css
└── lib/ # Additional Lua modules (optional)
└── *.lua
Important: When creating the archive, files must be at the root level (not inside a directory):
cd plugins/my-plugin
tar -czvf ../my-plugin.tar.gz manifest.toml init.lua signature.txt templates static
Creating a New Plugin
The easiest way to create a new plugin is with the amity-plugin tool:
# Create a new plugin with scaffolding
./target/release/amity-plugin init my-plugin --author "Your Name" --description "My plugin"
# This creates:
# my-plugin/
# ├── manifest.toml # Pre-configured manifest
# ├── init.lua # Entry point with lifecycle functions
# ├── templates/ # Example template
# ├── static/css/ # Example stylesheet
# ├── .luarc.json # LSP configuration for editors
# ├── types/amity.lua # Type definitions for autocompletion
# └── .gitignore
The generated plugin includes full LSP support for Lua Language Server, giving you autocompletion and documentation in editors like Neovim and VS Code.
manifest.toml
[plugin]
name = "my-plugin"
version = "1.0.0"
api_version = "v1"
description = "My awesome plugin"
author = "Your Name"
[dependencies]
# Other plugins this depends on (optional)
# other-plugin = ">=1.0.0"
[capabilities]
# Server-level capabilities (granted by admin via CLI)
declared = [
"http:register_route=/my-plugin",
"http:serve_static=/my-plugin/static",
"templates",
"navigation",
"kv:global=my-plugin:",
]
# User-level capabilities (granted by each user via consent flow)
# These are requested at runtime when the plugin needs them
user_declared = [
"storage:read=*",
"storage:write=/documents",
"kv:user",
]
[exports]
functions = ["my_function"]
Capability Types
Amity has two types of capabilities:
Server-level capabilities are granted by administrators via the CLI. They control what system resources the plugin can access:
| Capability | Description |
|---|---|
http:register_route=<prefix> |
Register HTTP routes under a path prefix |
http:external_request=<domains> |
Make HTTP requests to specific domains |
http:serve_static=<prefix> |
Serve static files under a path prefix |
websocket:channel=<name> |
Create/use WebSocket channels |
events:emit=<prefix> |
Emit events with a specific prefix |
events:listen=<prefix> |
Listen for events with a specific prefix |
kv:global=<prefix> |
Access plugin-wide KV storage |
kv:system |
Access system-tier KV storage |
user:enumerate |
List all users |
user:lookup |
Look up users by ID or name |
group:enumerate |
List all groups |
group:members |
List members of any group |
schedule:jobs |
Run scheduled background jobs |
http:modify_csp=<directives> |
Modify CSP headers |
http:modify_headers=<headers> |
Add custom HTTP headers |
templates |
Use the template system |
navigation |
Register navigation menu items |
navigation:admin_menu |
Register admin menu items |
http_sig |
Use HTTP signatures |
storage:admin |
Admin-level storage operations |
User-level capabilities are granted by each user individually via a consent flow when they first access a feature that requires the capability:
| Capability | Description |
|---|---|
storage:read=<folder> |
Read files from user's storage |
storage:write=<folder> |
Write files to user's storage |
storage:delete=<folder> |
Delete files from user's storage |
kv:user |
Access user's KV storage |
group:access=<types> |
Access user's groups |
crypto:encrypt |
Encrypt data with user's key |
crypto:decrypt |
Decrypt data with user's key |
profile:read |
Read user's profile information |
profile:write |
Modify user's profile information |
Signing Plugins
Plugins must be cryptographically signed with Ed25519 keys.
1. Generate a Signing Keypair
./target/release/amity-plugin sign keygen -o plugin-signing
This creates:
plugin-signing.private- Keep this secure!plugin-signing.public- Add to trusted keys
2. Add Public Key to Trusted Keys
sqlite3 amity.db "INSERT INTO trusted_signing_keys (name, public_key, is_active, created_at, instance_id) VALUES ('My Key', 'YOUR_PUBLIC_KEY_BASE64', 1, datetime('now'), 1);"
3. Sign a Plugin
# Create archive and sign in one step (signature is embedded automatically)
cd plugins/my-plugin
tar -czvf ../my-plugin.tar.gz manifest.toml init.lua templates static
cd ..
./target/release/amity-plugin sign archive -k plugin-signing.private my-plugin.tar.gz
4. Verify a Signature (Optional)
./target/release/amity-plugin sign verify -k plugin-signing.public my-plugin.tar.gz
5. Install Plugin
# Copy to plugin storage
mkdir -p storage/plugins
cp my-plugin.tar.gz storage/plugins/my-plugin-1.0.0.tar.gz
# Register in database
sqlite3 amity.db "INSERT INTO plugins (name, version, api_version, manifest_toml, archive_path, is_active, is_core, created_at, instance_id) VALUES ('my-plugin', '1.0.0', 'v1', 'MANIFEST_CONTENT', 'storage/plugins/my-plugin-1.0.0.tar.gz', 1, 0, datetime('now'), 1);"
amity-plugin Command Reference
# Initialize a new plugin
amity-plugin init <name> [options]
-a, --author <author> Plugin author
-d, --description <desc> Plugin description
-v, --version <version> Plugin version (default: 1.0.0)
--lib Include lib/ directory for Lua modules
-o, --output <dir> Output directory
# Signing commands
amity-plugin sign keygen -o <prefix> # Generate keypair
amity-plugin sign archive -k <key> <archive> # Sign an archive
amity-plugin sign verify -k <key> <archive> # Verify signature
amity-plugin sign show-public <private-key> # Show public key from private
Plugin Management CLI
# List installed plugins
amity plugin list
# Install a plugin
amity plugin install my-plugin.tar.gz # Install and enable
amity plugin install my-plugin.tar.gz --no-enable # Install without enabling
amity plugin install my-plugin.tar.gz --update # Update existing plugin
# Enable/disable plugins
amity plugin enable my-plugin
amity plugin disable my-plugin
# Remove a plugin
amity plugin remove my-plugin
amity plugin remove my-plugin --yes # Skip confirmation
# Manage trusted signing keys
amity plugin key list # List trusted keys
amity plugin key add <name> <public-key> # Add a trusted key
amity plugin key remove <key-id> # Remove a trusted key
# Grant server capabilities
amity plugin grant list my-plugin # List current grants
amity plugin grant all my-plugin # Grant all declared capabilities
amity plugin grant http:register_route my-plugin /api/my-plugin
amity plugin grant kv:system my-plugin
amity plugin grant templates my-plugin
amity plugin grant navigation my-plugin
# Revoke capabilities
amity plugin revoke my-plugin <grant-id> # Revoke specific grant
amity plugin revoke my-plugin --all # Revoke all grants
When running amity plugin grant all, server-level capabilities from the manifest are automatically granted. User-level capabilities (like storage:read, kv:user) are skipped with a message indicating they are granted per-user via the consent flow.
Development
Database Migrations
Migrations are automatically applied on server startup.
LSP Support for Plugin Development
The amity-plugin init command sets up LSP support automatically. If you're working on an existing plugin, you can copy the LSP files from the Amity repository:
# Copy to your plugin directory
cp /path/to/amity/.luarc.json ./
cp -r /path/to/amity/types ./
This provides:
- Autocompletion for all
amity.*APIs - Type hints and documentation on hover
- Parameter validation
Theming
Amity supports both built-in and custom themes. Users can select their preferred theme from the Profile page.
Built-in Themes
- System - Follows the operating system's light/dark preference
- Light - Light color scheme
- Dark - Dark color scheme
Custom Themes
Custom themes are CSS files that override CSS custom properties. They can be uploaded via the Admin > Themes page.
Creating a Custom Theme
Create a CSS file that targets the theme class and overrides the CSS custom properties:
/* my-theme.css */
.theme-my-theme {
/* Backgrounds */
--color-bg-primary: #1a1a2e;
--color-bg-secondary: #16213e;
--color-bg-tertiary: #1f2937;
/* Text colors */
--color-fg-primary: #e4e4e7;
--color-fg-secondary: #a1a1aa;
--color-fg-muted: #71717a;
/* Accent colors */
--color-accent-primary: #60a5fa;
--color-accent-primary-hover: #3b82f6;
/* Borders */
--border-color: #374151;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4);
/* Header */
--color-bg-header: #0f0f23;
--color-text-header: #ffffff;
--color-text-nav: #d4d4d8;
--color-text-nav-muted: #a1a1aa;
}
Important: The class name must match the theme ID prefixed with theme-. For example, a theme with ID my-theme must use the selector .theme-my-theme.
Available CSS Custom Properties
| Property | Description |
|---|---|
--color-bg-primary |
Main background color |
--color-bg-secondary |
Card/panel background |
--color-bg-tertiary |
Subtle background accent |
--color-fg-primary |
Primary text color |
--color-fg-secondary |
Secondary text color |
--color-fg-muted |
Muted/disabled text |
--color-accent-primary |
Primary accent (links, buttons) |
--color-accent-primary-hover |
Accent hover state |
--color-accent-success |
Success state color |
--color-accent-warning |
Warning state color |
--color-accent-danger |
Danger/error state color |
--border-color |
Default border color |
--shadow-sm |
Small shadow |
--shadow-md |
Medium shadow |
--shadow-lg |
Large shadow |
--color-bg-header |
Header background |
--color-text-header |
Header text |
--color-text-nav |
Navigation text |
--color-text-nav-muted |
Muted navigation text |
Uploading a Custom Theme
- Go to Admin > Themes
- Fill in the theme details:
- ID: Lowercase letters, numbers, hyphens, and underscores only (e.g.,
my-dark-theme) - Name: Display name shown to users
- Description: Optional description
- CSS: Paste the theme CSS content
- ID: Lowercase letters, numbers, hyphens, and underscores only (e.g.,
- Click Upload Theme
Theme API
The theming plugin exposes a REST API:
GET /theme/api/list - List all available themes
GET /theme/api/current - Get current user's theme preference
POST /theme/api/set - Set theme preference (JSON body: {"theme": "theme-id"})
GET /theme/api/css/:id - Get CSS for a custom theme
Lua Plugin API Reference
The amity global provides access to all plugin APIs.
Global Properties
amity.version -- Server version string (e.g., "2.0.0")
amity.api_version -- Plugin API version (e.g., "v1")
amity.log
Logging functions that output to the server log with plugin context.
amity.log.info(message) -- Info level log
amity.log.warn(message) -- Warning level log
amity.log.error(message) -- Error level log
amity.log.debug(message) -- Debug level log
amity.kv
Encrypted key-value storage with three tiers:
User-scoped storage (requires authenticated user):
-- Get/set values scoped to the current user
local value = amity.kv.get(key)
amity.kv.set(key, value)
amity.kv.del(key)
local keys = amity.kv.list(prefix) -- List keys with optional prefix
Shared storage (all-users group):
-- Read from shared storage (requires authentication)
local value = amity.kv.get_shared(key)
-- Write to shared storage (admin only)
amity.kv.set_shared(key, value)
System storage (server-level, always available):
-- Available to all plugins, encrypted with server key
local value = amity.kv.get_system(key)
amity.kv.set_system(key, value)
amity.kv.del_system(key)
local keys = amity.kv.list_system(prefix)
amity.http
HTTP route registration and utilities.
Route Registration:
-- Register a route handler
amity.http.register(method, path, handler)
-- Example handler
function plugin.handle_request(req)
-- req.method - HTTP method
-- req.path - Request path
-- req.params - Path parameters (e.g., :id)
-- req.query - Query parameters
-- req.headers - Request headers
-- req.body - Raw body string
-- req.json - Parsed JSON body (table)
-- req.form - Parsed form data (table)
-- req.user - Authenticated user or nil
-- req.user.id - User ID
-- req.user.username - Username
-- req.user.is_admin - Admin flag
return {
status = 200,
json = { message = "Hello" }
-- Or use: body, headers, template, context, text
}
end
amity.http.register("GET", "/api/hello", plugin.handle_request)
amity.http.register("POST", "/api/data/:id", function(req)
local id = req.params.id
return { status = 200, json = { id = id } }
end)
Response Types:
-- JSON response
return { status = 200, json = { key = "value" } }
-- Template response
return { status = 200, template = "my-template.html", context = { title = "Hello" } }
-- Plain text
return { status = 200, text = "Hello, World!" }
-- Raw body with headers
return { status = 200, body = "<html>...", headers = { ["Content-Type"] = "text/html" } }
Response Helpers:
amity.http.json(data) -- Create JSON response
amity.http.redirect(url, status?) -- Create redirect (default 302)
amity.http.error(status, message) -- Create error response
HTTP Fetch:
local response = amity.http.fetch(url, {
method = "POST",
headers = { ["Content-Type"] = "application/json" },
body = '{"key": "value"}',
timeout = 5000
})
-- response.status, response.headers, response.body
amity.nav
Navigation registration API.
-- Register a section (top-level navigation)
amity.nav.section(id, label, path, options?)
-- Register a tab within a section
amity.nav.tab(section_id, tab_id, label, path, options?)
-- Example: amity.nav.tab("admin", "theming", "Theming", "/admin/themes", { order = 60 })
-- Options:
-- {
-- icon = "home",
-- order = 100,
-- requires_auth = true,
-- requires_admin = false,
-- }
amity.template
Template rendering with Tera syntax.
-- Render a template file from the plugin's templates/ directory
local html = amity.template.render("my-template.html", { title = "Hello" })
-- Register an inline template
amity.template.add("inline-template", "<h1>{{ title }}</h1>")
-- Render a template string directly
local html = amity.template.render_string("<h1>{{ title }}</h1>", { title = "Hello" })
amity.time
Time utilities.
local timestamp = amity.time.now() -- Current Unix timestamp (seconds)
local millis = amity.time.now_millis() -- Current timestamp (milliseconds)
local iso = amity.time.iso8601() -- Current time as ISO 8601 string
local iso = amity.time.iso8601(timestamp) -- Convert timestamp to ISO 8601
local formatted = amity.time.format(timestamp, "%Y-%m-%d %H:%M:%S")
local ts = amity.time.parse("2024-01-15", "%Y-%m-%d")
amity.storage
Encrypted file storage API. Files are encrypted with group keys.
-- All storage operations require a group_id for encryption
local group_id = "group-uuid"
amity.storage.write(group_id, path, content)
local content = amity.storage.read(group_id, path)
amity.storage.delete(group_id, path)
local exists = amity.storage.exists(group_id, path)
local files = amity.storage.list(group_id, prefix)
local stat = amity.storage.stat(group_id, path) -- Returns {path, size, is_dir, ...}
amity.storage.mkdir(group_id, path)
amity.storage.copy(group_id, from, to)
amity.storage.rename(group_id, from, to)
-- List available storage backends
local backends = amity.storage.backends()
amity.notify
User notification system.
amity.notify.send(user_id, {
title = "Notification Title",
body = "Notification body",
type = "info", -- "info", "success", "warning", "error"
action_url = "/optional/link"
})
amity.notify.broadcast(group_id, notification) -- Send to all group members
local notifications = amity.notify.get(limit, offset, unread_only)
local count = amity.notify.unread_count(channel?)
amity.notify.mark_read(notification_id)
amity.notify.mark_all_read()
amity.notify.dismiss(notification_id)
amity.events
Event bus for plugin communication.
-- Subscribe to events
amity.events.on(event_name, handler)
amity.events.once(event_name, handler) -- One-time subscription
-- Emit events
amity.events.emit(event_name, data)
-- Example
amity.events.on("user:login", function(data)
amity.log.info("User logged in: " .. data.user_id)
end)
amity.schedule
Scheduled task execution.
-- Run every N seconds
local job_id = amity.schedule.every(60, function()
amity.log.info("Running every minute")
end)
-- Run on a cron schedule
local job_id = amity.schedule.cron("0 0 * * *", function()
amity.log.info("Running daily at midnight")
end)
-- Run once after delay
local job_id = amity.schedule.once(300, function()
amity.log.info("Running once after 5 minutes")
end)
-- Cancel a scheduled job
amity.schedule.cancel(job_id)
amity.capability
Plugin capability checking.
local has = amity.capability.has("http:register_route")
local granted = amity.capability.request("storage:write")
amity.websocket
WebSocket support.
amity.websocket.register("/ws/my-endpoint", {
on_connect = function(conn_id) end,
on_message = function(conn_id, message) end,
on_close = function(conn_id) end
})
amity.websocket.send(conn_id, message)
amity.websocket.close(conn_id)
amity.users
User management API (requires capabilities).
-- Get current authenticated user (always available)
local me = amity.users.current()
-- Returns: { id, username, display_name, is_admin }
-- Get user by ID
local user = amity.users.get(user_id)
-- Get user by username (requires user:lookup capability)
local user = amity.users.get_by_username("alice")
-- List all users (requires user:enumerate capability)
local users = amity.users.list({ limit = 50, offset = 0 })
-- Search users by username (requires user:enumerate capability)
local users = amity.users.search("ali")
-- List user's groups (requires group:access user capability)
local groups = amity.users.groups(user_id, { type = "share" })
-- List members of a group (requires group:members server capability)
local members = amity.users.group_members(group_id)
amity.http_sig
HTTP signature support for federation protocols.
-- Sign a request using Cavage draft (Mastodon/Fediverse compatibility)
local headers = amity.http_sig.sign_cavage("POST", "https://remote.server/inbox", body)
-- Returns: { Signature, Date, Digest }
-- Sign a request using RFC 9421 (OCM/modern protocols)
local headers = amity.http_sig.sign_rfc9421("POST", "https://remote.server/shares", "application/json", body)
-- Verify a signature from an incoming request
local valid = amity.http_sig.verify(public_key_pem, key_id, method, path, headers, body)
-- Get this server's public key for federation
local pem = amity.http_sig.get_public_key_pem()
local info = amity.http_sig.get_key_info() -- { key_id, algorithm }
amity.mls
MLS group encryption for secure data sharing.
-- Create a new encrypted group
amity.mls.create_group(group_id, "share") -- Types: share, chat, team
-- Encrypt/decrypt data with group key
local ciphertext = amity.mls.encrypt(group_id, plaintext)
local plaintext = amity.mls.decrypt(group_id, ciphertext)
-- Group membership
local is_member = amity.mls.is_member(group_id)
local role = amity.mls.get_role(group_id) -- "admin", "member", etc.
local members = amity.mls.list_members(group_id)
-- Manage members
amity.mls.invite_member(group_id, user_id, "member")
amity.mls.accept_invitation(group_id)
local invites = amity.mls.pending_invitations()
amity.mls.remove_member(group_id, user_id)
amity.export / amity.import
Inter-plugin function sharing.
-- In plugin A: export a function
amity.export("my_function", function(arg)
return "result: " .. arg
end)
-- In plugin B: import and call
local fn = amity.import("plugin_a", "my_function")
if fn then
local result = fn("test")
end
Plugin Lifecycle
plugin = {}
function plugin.init()
-- Called when plugin is loaded
amity.log.info("Plugin initialized")
-- Register routes, navigation, etc.
if amity.http then
plugin.register_routes()
end
end
function plugin.register_routes()
amity.http.register("GET", "/my-plugin", plugin.handle_index)
end
function plugin.enable()
-- Called when plugin is enabled
end
function plugin.disable()
-- Called when plugin is disabled
end
function plugin.unload()
-- Called when plugin is unloaded
end
return plugin
Architecture
src/- Main application sourcebin/- Binary entry points (amity,amity-plugin)db/- Database modelsplugin/- Plugin loading, sandbox, and APIsweb/- HTTP routes and templatesauth/- Authentication services
plugins/- Core plugin source filesstorage/plugins/- Installed plugin archivestemplates/- Core HTML templatesmigration/- SeaORM database migrationstypes/- Lua type definitions for LSP support