# Mooncore
A lightweight, action-based web framework for Elixir. A Phoenix alternative built around the action pattern.
## Core Concept
Every feature is an **action** — a named operation mapped to a module function. Actions are transport-agnostic: the same action works via HTTP, WebSocket, local Elixir call, or any other protocol.
```
┌─── HTTP (POST /run)
│
Action.run/2 ◄─┼─── WebSocket
│
├─── Local Elixir call
│
└─── Any protocol you add
```
## 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
```elixir
defmodule MyApp.Action do
use Mooncore.Action
@actions %{
"echo" => {MyApp.Action.Echo, :echo, [], %{}},
"task.create" => {MyApp.Action.Task, :create, ~w(user), %{}},
"task.list" => {MyApp.Action.Task, :list, ~w(user), %{}},
}
end
defmodule MyApp.Action.Echo do
def echo(req), do: %{echo: req.params}
end
defmodule MyApp.Action.Task do
def create(req) do
record = req.params["record"]
# ... your persistence logic ...
# Publish to WebSocket clients:
Mooncore.Endpoint.Socket.publish(req[:dkey], {"record-add", record})
{:ok, record}
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", "record": {"title": "Test"}}
# From WebSocket — handled by socket handler
# {"action": "task.create", "record": {"title": "Test"}, "rayid": "abc"}
# From Elixir code — no transport needed
MyApp.Action.run("task.create", %{
params: %{"record" => %{"title" => "Test"}},
auth: %{"roles" => ["user"]}
})
# Through the middleware pipeline
Mooncore.Action.execute("task.create", %{
params: %{"record" => %{"title" => "Test"}},
auth: %{"roles" => ["user"]}
})
```
## 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 |
## License
MIT