- Add arch/ directory with PKGBUILD for Arch Linux packages - Split packages: amity (server) and amity-plugin (dev tool) - Use systemd sysusers/tmpfiles for user and directory creation - Add install hooks for default config generation - Update Dockerfile to build deb package in Docker using pre-built binaries - Add make arch target for building Arch packages - Update make docker to require pre-built binaries |
||
|---|---|---|
| arch | ||
| debian | ||
| migration | ||
| plugins | ||
| src | ||
| static/data | ||
| storage/themes | ||
| templates | ||
| themes | ||
| types | ||
| .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"
[capabilities]
declared = [
"http:register_route",
"static_files",
"templates",
]
[exports]
functions = ["my_function"]
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
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.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