A lightweight, self-hosted Function-as-a-Service platform written in Go with Lua scripting.
Beta Phase Notice: This project is currently in beta. New features and changes are actively being developed, but I promise to maintain backward compatibility for all Lua APIs.
- Simple Lua Functions - Write serverless functions in Lua
- Code Editor - Monaco Editor with autocomplete and inline documentation
- HTTP Triggers - Execute functions via HTTP requests
- Built-in APIs - HTTP client, KV store, environment variables, logging, and more
- AI Integration - Chat completions with OpenAI and Anthropic, with request/response logging
- Email Integration - Send emails via Resend with scheduling support
- Version Control - Track and manage function versions
- Execution History - Monitor function executions and logs
- Beautiful Error Messages - Human-friendly error messages with code context, line numbers, and actionable suggestions
- Web Dashboard - Manage functions through a clean web interface
- API Documentation - Swagger UI available at
/docs - Lightweight - Single binary, no external dependencies
- Go 1.26 or newer
make- Chrome or Chromium if you plan to run the E2E test suite
For CLI internals and code generation details, see cli/README.md.
git clone https://github.com/dimiro1/lunar.git
cd lunar
make build./build/lunarFor local development, you can also install the optional contributor tools:
make install-toolsThis installs air for live reload and goreleaser for release packaging.
Then start the development server with:
make devThe application will be available at http://localhost:3000.
On first run, Lunar will automatically generate an API key and save it to data/api_key.txt. The key will be printed in the server logs:
INFO Generated new API key key=cf31cb0cdc7811ca9cec6a3c77579b3ea28c1e4e10d6fc1061ae71788834c21b file=data/api_key.txt
When you access the dashboard, you'll be prompted to enter this API key to login. The key is also available in the data/api_key.txt file.
- Open
http://localhost:3000and log in with the API key fromdata/api_key.txt. - Create a new function named
hello-world. - Paste the sample handler below and save it.
- Copy the function ID and invoke it:
curl http://localhost:3000/fn/<function-id>You should get back a JSON response. After that, open the function's execution history in the dashboard to inspect logs and request details.
Functions are written in Lua and must export a handler function:
function handler(ctx, event)
-- ctx contains execution context (executionId, functionId, etc.)
-- event contains HTTP request data (method, path, query, body, headers)
log.info("Function started")
return {
statusCode = 200,
headers = { ["Content-Type"] = "application/json" },
body = json.encode({ message = "Hello, World!" })
}
end- log - Logging utilities (info, debug, warn, error)
- kv - Key-value storage (get, set, delete)
- env - Environment variables (get)
- http - HTTP client (get, post, put, delete)
- json - JSON encoding/decoding
- crypto - Cryptographic functions (md5, sha256, hmac, uuid)
- time - Time utilities (now, format, sleep)
- url - URL utilities (parse, encode, decode)
- strings - String manipulation
- random - Random generators
- base64 - Base64 encoding/decoding
- ai - AI chat completions (OpenAI, Anthropic)
- email - Send emails via Resend
Lunar provides an llms.txt file at /llms.txt with the complete Lua API reference, including function signatures, parameters, and code examples. You can use this with any LLM-powered coding assistant to get accurate help when writing Lunar functions.
function handler(ctx, event)
-- Get current count from KV store
local count = kv.get("counter") or "0"
local newCount = tonumber(count) + 1
-- Save updated count
kv.set("counter", tostring(newCount))
log.info("Counter incremented to: " .. newCount)
return {
statusCode = 200,
headers = { ["Content-Type"] = "application/json" },
body = json.encode({ count = newCount })
}
end-- Requires RESEND_API_KEY environment variable
function handler(ctx, event)
local data = json.decode(event.body)
local result, err = email.send({
from = "noreply@yourdomain.com",
to = data.email,
subject = "Welcome!",
html = "<h1>Hello, " .. data.name .. "!</h1>",
scheduled_at = time.now() + 3600 -- Optional: send in 1 hour
})
if err then
return {
statusCode = 500,
body = json.encode({ error = err })
}
end
return {
statusCode = 200,
headers = { ["Content-Type"] = "application/json" },
body = json.encode({ email_id = result.id })
}
endcurl -X GET http://localhost:3000/fn/{function-id}
curl -X POST http://localhost:3000/fn/{function-id} -d '{"key":"value"}'
curl -X GET http://localhost:3000/fn/{function-id}?name=John# Run the latest release from Docker Hub
docker run -p 3000:3000 -v $(pwd)/data:/data dimiro1/lunar:latest
# Build and run with Docker
docker build -t lunar .
docker run -p 3000:3000 -v lunar-data:/app/data lunar
# Or use Docker Compose
docker compose up -dLunar is ready to deploy on Railway:
- Connect Repository - Link your GitHub repository to Railway
- Add Volume - Create a volume and mount it to
/data - Set Environment Variables:
BASE_URL- Your Railway public URL (e.g.,https://yourapp.up.railway.app)API_KEY- (Optional) Set a custom API key, or let it auto-generate
- Deploy - Railway will automatically detect the Dockerfile and deploy
The Dockerfile is Railway-compatible and will:
- Use Railway's automatic
PORTenvironment variable - Bind to
0.0.0.0:$PORTfor public networking - Persist data to the mounted volume at
/data
Lunar can be configured via environment variables:
PORT=3000 # HTTP server port (default: 3000)
DATA_DIR=./data # Data directory for SQLite database (default: ./data)
EXECUTION_TIMEOUT=300 # Function execution timeout in seconds (default: 300)
API_KEY=your-key-here # API key for authentication (auto-generated if not set)
BASE_URL=http://localhost:3000 # Base URL for the deployment (auto-detected if not set)The dashboard requires authentication via API key. You can:
- Auto-generate (recommended) - Let Lunar generate a secure key on first run
- Set manually - Provide your own key via the
API_KEYenvironment variable
API calls can authenticate using either:
- Cookie - Automatically handled by the dashboard after login
- Bearer token - Include
Authorization: Bearer YOUR_API_KEYheader
Example API call with Bearer token:
curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:3000/api/functionsNote: Function execution endpoints (/fn/{id}) do not require authentication.
Lunar ships a command-line client (lunar-cli) that is auto-generated from the OpenAPI spec, so it always stays in sync with the API.
go install github.com/dimiro1/lunar/lunar-cli@latestOr download a pre-built binary from the Releases page (lunar-cli_* archives).
AI agent skills require the CLI to be installed first.
Lunar ships built-in skill definitions that teach your AI coding agent how to use the CLI and write Lua functions.
lunar-cli skills list # show available skills
lunar-cli skills show lunar-cli # CLI command reference
lunar-cli skills show lunar-lua # Lua function authoring guideTo install them, ask your agent:
"Install the Lunar skills from the
lunar-cli skillscommand."
# Start the device authorization flow (opens a browser tab for approval)
lunar-cli --server http://your-lunar-server login
# If the browser does not open automatically, use the printed approval URL and code.
# The token is saved to ~/.config/lunar/config.yaml automatically
# To log out and clear the stored token:
lunar-cli logoutYou can also skip the login flow and pass a token directly:
lunar-cli --token YOUR_API_KEY functions list
# Or via environment variable:
export LUNAR_SERVER=http://your-lunar-server
export LUNAR_TOKEN=YOUR_API_KEY
lunar-cli functions listThe CLI stores its configuration in ~/.config/lunar/config.yaml:
server: http://localhost:3000
token: <your-api-token>Flags and environment variables always take precedence over the config file:
| Priority | Source |
|---|---|
| 1 (highest) | --server / --token flags |
| 2 | LUNAR_SERVER / LUNAR_TOKEN env vars |
| 3 | ~/.config/lunar/config.yaml |
lunar-cli functions list [--limit 20] [--offset 0]
lunar-cli functions create --name hello-world --code handler.lua
lunar-cli functions create --name hello-world --code - # read code from stdin
lunar-cli functions get <id>
lunar-cli functions update <id> --name new-name
lunar-cli functions update <id> --cron-schedule "*/5 * * * *" --cron-status active
lunar-cli functions update <id> --disabled
lunar-cli functions delete <id>
lunar-cli functions env <id> --env API_KEY=secret --env DEBUG=true
lunar-cli functions kv <id> --kv counter=0 --kv state=idle
lunar-cli functions kv <id> --kv shared=value --global # write to global KV
lunar-cli functions next-run <id>lunar-cli versions list <function-id>
lunar-cli versions get <function-id> <version-number>
lunar-cli versions activate <function-id> <version-id>
lunar-cli versions delete <function-id> <version-id>
lunar-cli versions diff <function-id> <v1> <v2>lunar-cli executions list <function-id>
lunar-cli executions get <execution-id>
lunar-cli executions logs <execution-id>
lunar-cli executions ai-requests <execution-id>
lunar-cli executions email-requests <execution-id>lunar-cli tokens list
lunar-cli tokens revoke <token-id>lunar-cli llmsExecute a function directly without authentication (functions are public by default):
lunar-cli invoke <function-id>
lunar-cli invoke <function-id> --method POST --body '{"key":"value"}'
lunar-cli invoke <function-id> --method POST --body - # read body from stdinThe CLI commands are auto-generated from internal/api/docs/openapi.yaml. When the API changes, regenerate with:
cd cli
go generate ./...
go build ./...Run the Go unit tests:
make testThe frontend uses Jasmine for unit testing, running directly in the browser without Node.js dependencies.
make test-frontendThis starts a local Go server and opens the test runner at http://localhost:8888/test/SpecRunner.html. Tests cover:
- Route URL generators
- UI components (Button, Badge, Table, Pagination, ...)
End-to-end tests use chromedp to run a headless Chrome browser:
Make sure Chrome or Chromium is installed before running them.
make test-e2eE2E tests cover:
- Login flow
- Page navigation
- Functions list rendering
make test-allThis runs Go unit tests and E2E tests. Run make test-frontend separately to open the browser-based Jasmine tests.
- Backend - Go with standard library HTTP server, SQLite database
- Frontend - Mithril.js SPA with Monaco Editor
- Runtime - GopherLua for Lua script execution
- Storage - SQLite for functions, versions, executions, KV store, and environment variables
JavaScript dependencies are vendored in frontend/vendor/ (no npm required). Versions are managed in the Makefile:
| Library | Purpose |
|---|---|
| Mithril.js | SPA framework |
| Monaco Editor | Code editor |
| Highlight.js | Syntax highlighting |
| Jasmine | Frontend testing |
To update dependencies, edit the version variables in the Makefile and run:
make vendor-jsContributions are welcome! Please feel free to submit issues or pull requests.
Claudemiro Alves Feitosa Neto
MIT License - see LICENSE file for details.











