No description
Find a file
Micke Nordin 109cebc28c Add Arch Linux packaging and improve Docker build
- 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
2026-01-12 12:11:25 +01:00
arch Add Arch Linux packaging and improve Docker build 2026-01-12 12:11:25 +01:00
debian Use schema traits as single source of truth for Lua API types 2026-01-08 21:01:04 +01:00
migration Implement missing plugin API features from ARCHITECTURE.md 2026-01-08 21:53:21 +01:00
plugins Add frontend improvements and utility APIs 2026-01-11 15:10:07 +01:00
src Add cache busting for plugin static files via ETag 2026-01-11 19:59:27 +01:00
static/data Add JavaScript API for plugins with autocomplete and TypeScript tooling 2026-01-09 13:17:36 +01:00
storage/themes Auto-package and sign plugins during cargo build 2026-01-08 22:29:41 +01:00
templates Add frontend improvements and utility APIs 2026-01-11 19:55:31 +01:00
themes Production readiness fixes: auth, scheduling, websockets, OCM shares 2026-01-08 16:56:05 +01:00
types Add url_encode to amity.utils API 2026-01-11 18:51:00 +01:00
.env.example Add Docker setup for containerized deployment 2026-01-11 18:07:12 +01:00
.gitignore Add Arch Linux packaging and improve Docker build 2026-01-12 12:11:25 +01:00
.luarc.json Add LSP support and improve theming plugin 2026-01-08 14:48:15 +01:00
ARCHITECTURE.md Implement missing plugin API features from ARCHITECTURE.md 2026-01-08 21:53:21 +01:00
build Rewrite Amity with plugin architecture (v2.0.0) 2026-01-07 15:48:11 +01:00
build.rs Add frontend improvements and utility APIs 2026-01-11 15:10:07 +01:00
cargo Rewrite Amity with plugin architecture (v2.0.0) 2026-01-07 15:48:11 +01:00
Cargo.lock Add JavaScript API for plugins with autocomplete and TypeScript tooling 2026-01-09 13:17:36 +01:00
Cargo.toml Add JavaScript API for plugins with autocomplete and TypeScript tooling 2026-01-09 13:17:36 +01:00
docker-compose.yml Add Docker setup for containerized deployment 2026-01-11 18:07:12 +01:00
Dockerfile Add Arch Linux packaging and improve Docker build 2026-01-12 12:11:25 +01:00
Makefile Add Arch Linux packaging and improve Docker build 2026-01-12 12:11:25 +01:00
README.md Add LSP support and improve theming plugin 2026-01-08 14:48:15 +01:00
TODO.md Implement SFTP share authentication with SSH keys (OCM compliant) 2026-01-08 17:21:58 +01:00

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

  1. Go to Admin > Themes
  2. 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
  3. 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 source
    • bin/ - Binary entry points (amity, amity-plugin)
    • db/ - Database models
    • plugin/ - Plugin loading, sandbox, and APIs
    • web/ - HTTP routes and templates
    • auth/ - Authentication services
  • plugins/ - Core plugin source files
  • storage/plugins/ - Installed plugin archives
  • templates/ - Core HTML templates
  • migration/ - SeaORM database migrations
  • types/ - Lua type definitions for LSP support