<p align="center">
<img src="guides/mooncore.png" alt="Mooncore" width="120" />
</p>
# Mooncore
A lightweight, action-based api framework for Elixir.
## Why Actions, Not REST
REST was designed for documents — CRUD operations on URL-addressable resources. It works, but it forces you to think in terms of HTTP: which verb, which URL path, which status code, how to nest resources, how to handle batch operations that don't fit the resource model.
Mooncore replaces all of that with a single concept: **actions**. Every feature is a named function call.
```
REST Mooncore
──── ────────
GET /api/tasks "task.list"
POST /api/tasks "task.create"
PUT /api/tasks/:id "task.update"
DELETE /api/tasks/:id "task.delete"
POST /api/tasks/:id/assign "task.assign"
POST /api/tasks/batch-archive "task.batch_archive"
GET /api/reports/weekly?... "report.weekly"
```
No routing tables, no path params, no verb selection, no "is this a PUT or PATCH" debates. Just action names and parameters.
### Transport Independence
Actions don't know how they were called. The same `"task.create"` works across every transport without modification:
```
┌─── HTTP POST /run
│
├─── WebSocket message
│
Action.run/2 ◄─┼─── Elixir function call
│
├─── MCP tool call (AI agents)
│
├─── Protobuf / gRPC adapter
│
├─── Message queue consumer
│
└─── Cron scheduler
```
With REST, adding WebSocket support means rebuilding your entire API layer. With actions, you write the logic once and plug in transports. Need a NATS consumer that triggers `"order.process"`? It's one adapter that calls `Action.execute/2` — your handler doesn't change.
### AI-Native Development
Actions are pure functions: a name, a parameter map, a result. This is the ideal interface for AI-assisted development — an agent can generate a handler, call it through the built-in MCP server, inspect the result, and iterate. No HTTP client setup, no URL construction, no status code interpretation. Just `{"action": "task.create", "title": "Buy milk"}`.
Because Mooncore includes an MCP server out of the box, AI agents connect directly to the running application. They discover available actions, execute them, read logs, and evaluate code — all through the same action interface your frontend uses. The functional style (map in, map out) means agents produce correct code faster with fewer tokens, since there's no framework boilerplate or object hierarchy to reason about.
### Clean Separation
Because actions are transport-agnostic, your application logic has zero coupling to HTTP, WebSocket, or any delivery mechanism. This means:
- **UI logic stays in the UI.** Your backend is a flat list of operations, not a REST hierarchy that mirrors your page structure.
- **Testing is trivial.** Call the action function directly with a map. No HTTP client, no router, no connection struct.
- **New protocols are adapters, not rewrites.** Add protobuf, GraphQL, message queues, or schedulers without touching business logic.
## Installation
Add to your `mix.exs`:
```elixir
def deps do
[
{:mooncore, "~> 0.1.0"}
]
end
```
## Quick Start
### 1. Define your app
```elixir
defmodule MyApp.Roles do
@roles ["admin", "manager", "user", "guest"]
def list, do: @roles
end
defmodule MyApp.App do
@behaviour Mooncore.App
@impl true
def list do
%{
"myapp" => %{
key: "myapp",
name: "My Application",
roles: MyApp.Roles.list(),
action_module: MyApp.Action
}
}
end
@impl true
def info(app_name), do: Map.get(list(), app_name)
end
```
### 2. Define actions
> **Important:** `@actions` must be defined **before** `use Mooncore.Action`.
> The macro captures `@actions` at compile time.
```elixir
defmodule MyApp.Action do
@actions %{
"echo" => {MyApp.Action.Echo, :echo, [], %{}},
"task.create" => {MyApp.Action.Task, :create, ~w(user), %{}},
"task.list" => {MyApp.Action.Task, :list, ~w(user), %{}},
}
use Mooncore.Action
end
defmodule MyApp.Action.Echo do
def echo(req), do: %{echo: req[:params]}
end
defmodule MyApp.Action.Task do
def create(req) do
# req[:params] is the full request body:
# %{"action" => "task.create", "title" => "Buy milk", ...}
title = req[:params]["title"]
# ... your persistence logic ...
# Publish to WebSocket clients:
Mooncore.Endpoint.Socket.publish(req[:auth]["dkey"], {"task-created", %{title: title}})
{:ok, %{title: title}}
end
def list(req) do
# ... your query logic ...
{:ok, []}
end
end
```
### 3. Define your router
You own the router. Mooncore provides plugs and helpers, you compose them however you want:
```elixir
defmodule MyApp.Router do
use Plug.Router
plug Plug.Logger
plug CORSPlug, origin: ["*"]
plug Mooncore.Auth.Plug
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, {:json, json_decoder: Jason}],
length: 100_000_000
plug :match
plug :dispatch
# Action endpoint — Mooncore handles dispatch + JSON response
match "/run" do
Mooncore.Endpoint.Http.handle(conn)
end
# Or handle it yourself for custom HTTP responses:
match "/api" do
result = Mooncore.Endpoint.Http.receive_action(conn)
case result do
%{error: "not found" <> _} ->
conn |> put_resp_header("content-type", "application/json")
|> send_resp(404, Jason.encode!(result))
_ ->
conn |> put_resp_header("content-type", "application/json")
|> send_resp(200, Jason.encode!(Mooncore.Action.format_response(result)))
end
end
# WebSocket
get "/ws" do
conn
|> WebSockAdapter.upgrade(Mooncore.Endpoint.Socket.Handler, [conn: conn], timeout: 60_000)
|> halt()
end
# Your custom routes
get "/" do
send_resp(conn, 200, "My App")
end
match _ do
send_resp(conn, 404, "Not Found")
end
end
```
### 4. Configure
```elixir
# config/config.exs
config :mooncore,
port: 4000,
router: MyApp.Router,
app_module: MyApp.App,
jwt: [
key: System.get_env("JWT_KEY"),
issuer: "myapp"
],
pools: [:default],
before_action: [],
after_action: []
```
### 5. Call actions from anywhere
```elixir
# From HTTP — handled by router
# POST /run {"action": "task.create", "title": "Test"}
# From WebSocket — handled by socket handler
# {"action": "task.create", "title": "Test", "rayid": "abc"}
# From Elixir code — no transport needed
MyApp.Action.run("task.create", %{
params: %{"action" => "task.create", "title" => "Test"},
auth: %{"roles" => ["user"]}
})
# Through the middleware pipeline
Mooncore.Action.execute("task.create", %{
params: %{"action" => "task.create", "title" => "Test"},
auth: %{"roles" => ["user"]}
})
```
### 6. Run
```bash
mix run --no-halt
```
`Mooncore.Application` starts the Bandit HTTP server automatically — you don't need to add anything to your own supervision tree.
## Middleware
Add before/after hooks to the action pipeline:
```elixir
defmodule MyApp.Middleware.DBLink do
@behaviour Mooncore.Middleware
@impl true
def call(req) do
db = MyApp.DB.resolve(req[:auth]["dkey"])
Map.put(req, :db, db)
end
end
defmodule MyApp.Middleware.AuditLog do
@behaviour Mooncore.Middleware
@impl true
def call(response) do
# Log the response, strip sensitive data, etc.
if is_map(response), do: Map.delete(response, "password"), else: response
end
end
# config/config.exs
config :mooncore,
before_action: [MyApp.Middleware.DBLink],
after_action: [MyApp.Middleware.AuditLog]
```
## WebSocket
Real-time pub/sub with channel support:
```elixir
# Publish to connected clients
Mooncore.Endpoint.Socket.publish("group_key", {"event", data})
Mooncore.Endpoint.Socket.publish("group_key", {"event", data}, ["main:default", "chat:lobby"])
# Clients connect to /ws and can:
# - Authenticate: ["jwt", "token_string"]
# - Join channels: ["join", "channel_name"] (requires "channel_<name>" role)
# - Leave channels: ["leave", "channel_name"]
# - Send actions: {"action": "...", "params": {...}, "rayid": "..."}
# - Ping: "ping" → "pong"
```
## MCP Server (AI Observability)
Query your running app's internals:
```elixir
Mooncore.MCP.Server.list_actions() # All registered actions
Mooncore.MCP.Server.list_clients() # Connected WebSocket clients
Mooncore.MCP.Server.list_apps() # Registered apps
Mooncore.MCP.Server.server_info() # Server configuration
```
## Action Definition Format
```elixir
"action.name" => {Module, :function, required_roles, request_modifications}
```
| Component | Description |
| ----------------------- | --------------------------------------------------- |
| `"action.name"` | Unique string identifier, dot-notation |
| `Module` | Handler module |
| `:function` | Function atom |
| `required_roles` | `[]` = public, `~w(user)` = requires "user" role |
| `request_modifications` | Map deep-merged into request before calling handler |
## For AI Agents
If you are an AI coding agent, read [`guides/skills.md`](guides/skills.md) before generating Mooncore code. It contains scaffolding templates, critical rules and common patterns.
## License
MIT