hb_http_server.erl - HTTP Server & AO-Core Router
Overview
Purpose: HTTP server routing requests to AO-Core resolver
Module: hb_http_server
Behavior: Cowboy HTTP handler
Pattern: HTTP request → Message → AO-Core → Message → HTTP response
This module implements a Cowboy-based HTTP server that marshals incoming HTTP requests into HyperBEAM messages, passes them to the AO-Core resolver, and converts the results back into HTTP responses. The server configuration is stored in Cowboy's environment and can be dynamically updated.
Core Responsibilities
- HTTP Server Lifecycle: Start, configure, and manage HTTP listeners
- Request Marshaling: Convert HTTP requests to HyperBEAM messages
- AO-Core Integration: Route messages through singleton resolution
- Response Conversion: Transform message results to HTTP responses
- Configuration Management: Dynamic server options via Cowboy environment
- Hook System: Execute startup hooks for configuration modification
- Greeting Display: ASCII art banner with configuration on startup
Dependencies
- HyperBEAM:
hb_http,hb_ao,hb_util,hb_maps,hb_opts,hb_message,hb_singleton,hb_store,hb_format,hb_private,hb_features - Arweave:
ar_wallet - Hooks:
dev_hook - HTTP:
cowboy,cowboy_req,ranch,gun - OTP:
kernel,stdlib,inets,ssl,os_mon - Metrics:
prometheus,prometheus_cowboy - Includes:
include/hb.hrl
Public Functions Overview
%% Server Lifecycle
-spec start() -> {ok, pid()}.
-spec start(Opts) -> {ok, pid()}.
-spec start_node() -> NodeURL.
-spec start_node(Opts) -> NodeURL.
%% Configuration Management
-spec set_opts(Opts) -> ok.
-spec set_opts(Request, Opts) -> {ok, UpdatedOpts}.
-spec get_opts() -> Opts.
-spec get_opts(NodeMsg) -> Opts.
-spec set_default_opts(Opts) -> Opts.
-spec set_proc_server_id(ServerID) -> ok.
%% Cowboy Callbacks
-spec init(Req, ServerID) -> {ok, Req, State}.
-spec allowed_methods(Req, State) -> {[Method], Req, State}.Public Functions
1. start/0, start/1
-spec start() -> {ok, pid()}
when
pid() :: pid().
-spec start(Opts) -> {ok, pid()}
when
Opts :: map(),
pid() :: pid().Description: Start the HTTP server with configuration from file or provided options. Initializes store, loads wallet, displays greeting banner, and starts Cowboy listener.
Startup Flow:Load Config File (config.flat)
↓
Merge with Environment Defaults
↓
Initialize Store
↓
Load Private Wallet
↓
Display Greeting Banner
↓
Execute Startup Hooks
↓
Start Cowboy Listener
↓
Return {ok, ListenerPID}-module(hb_http_server_start_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
start_with_opts_test() ->
Config = #{
port => 10000 + rand:uniform(10000),
store => [hb_test_utils:test_store()],
priv_wallet => ar_wallet:new()
},
{ok, Pid} = hb_http_server:start(Config),
?assert(is_pid(Pid)).
start_returns_listener_test() ->
Config = #{
port => 10000 + rand:uniform(10000),
priv_wallet => ar_wallet:new()
},
Result = hb_http_server:start(Config),
?assertMatch({ok, _}, Result).2. start_node/0, start_node/1
-spec start_node() -> NodeURL
when
NodeURL :: binary().
-spec start_node(Opts) -> NodeURL
when
Opts :: map(),
NodeURL :: binary().Description: Start a test node with random port and return its URL. Used primarily for testing.
Implementation:start_node(Opts) ->
application:ensure_all_started([kernel, stdlib, inets, ssl, ranch, cowboy, gun, os_mon]),
hb:init(),
hb_sup:start_link(Opts),
ServerOpts = set_default_opts(Opts),
{ok, _Listener, Port} = new_server(ServerOpts),
<<"http://localhost:", (integer_to_binary(Port))/binary, "/">>.-module(hb_http_server_node_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
start_node_test() ->
Node = hb_http_server:start_node(),
?assert(is_binary(Node)),
?assert(binary:match(Node, <<"http://localhost:">>) =/= nomatch).
start_node_with_opts_test() ->
Node = hb_http_server:start_node(#{
<<"test-key">> => <<"test-value">>
}),
{ok, Info} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
?assertEqual(<<"test-value">>, hb_ao:get(<<"test-key">>, Info, #{})).
start_node_returns_url_test() ->
Node = hb_http_server:start_node(#{}),
% URL should end with /
?assertEqual(<<"/">>, binary:part(Node, byte_size(Node) - 1, 1)).
start_node_accessible_test() ->
Node = hb_http_server:start_node(#{}),
% Should be able to get info from the node
{ok, Info} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
?assert(is_map(Info)).3. set_opts/1, set_opts/2
-spec set_opts(Opts) -> ok
when
Opts :: map().
-spec set_opts(Request, Opts) -> {ok, UpdatedOpts}
when
Request :: map(),
Opts :: map(),
UpdatedOpts :: map().Description: Update server configuration dynamically. Single-arity version updates Cowboy environment; two-arity version merges request with options and maintains history.
set_opts/1 - Update Cowboy Environment:set_opts(Opts) ->
case hb_opts:get(http_server, no_server_ref, Opts) of
no_server_ref -> ok;
ServerRef -> cowboy:set_env(ServerRef, node_msg, Opts)
end.set_opts(Request, Opts) ->
PreparedOpts = hb_opts:mimic_default_types(Opts, false, Opts),
PreparedRequest = hb_opts:mimic_default_types(
hb_message:uncommitted(Request),
false,
Opts
),
MergedOpts = maps:merge(PreparedOpts, PreparedRequest),
History = hb_opts:get(node_history, [], Opts) ++ [ResetRequest],
FinalOpts = MergedOpts#{
http_server => hb_opts:get(http_server, no_server, Opts),
node_history => History
},
{set_opts(FinalOpts), FinalOpts}.-module(hb_http_server_set_opts_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
set_opts_merge_test() ->
Wallet = ar_wallet:new(),
_Node = hb_http_server:start_node(#{priv_wallet => Wallet}),
% Get initial options
Opts = hb_http_server:get_opts(#{
http_server => hb_util:human_id(ar_wallet:to_address(Wallet))
}),
% Update with new request
Request = #{<<"hello">> => <<"world">>},
{ok, UpdatedOpts} = hb_http_server:set_opts(Request, Opts),
% Verify merge happened
?assertEqual(<<"world">>, hb_opts:get(<<"hello">>, not_found, UpdatedOpts)).
set_opts_history_test() ->
Wallet = ar_wallet:new(),
_Node = hb_http_server:start_node(#{priv_wallet => Wallet}),
Opts = hb_http_server:get_opts(#{
http_server => hb_util:human_id(ar_wallet:to_address(Wallet))
}),
% First update
{ok, Opts1} = hb_http_server:set_opts(#{<<"key1">> => <<"val1">>}, Opts),
History1 = hb_opts:get(node_history, [], Opts1),
?assertEqual(1, length(History1)),
% Second update
{ok, Opts2} = hb_http_server:set_opts(#{<<"key2">> => <<"val2">>}, Opts1),
History2 = hb_opts:get(node_history, [], Opts2),
?assertEqual(2, length(History2)).
set_opts_preserves_server_ref_test() ->
Wallet = ar_wallet:new(),
_Node = hb_http_server:start_node(#{priv_wallet => Wallet}),
ServerRef = hb_util:human_id(ar_wallet:to_address(Wallet)),
Opts = hb_http_server:get_opts(#{http_server => ServerRef}),
{ok, UpdatedOpts} = hb_http_server:set_opts(#{<<"new">> => <<"data">>}, Opts),
?assertEqual(ServerRef, hb_opts:get(http_server, not_found, UpdatedOpts)).4. get_opts/0, get_opts/1
-spec get_opts() -> Opts
when
Opts :: map().
-spec get_opts(NodeMsg) -> Opts
when
NodeMsg :: map(),
Opts :: map().Description: Retrieve current server configuration from Cowboy environment.
Implementation:get_opts() ->
get_opts(#{http_server => get(server_id)}).
get_opts(NodeMsg) ->
ServerRef = hb_opts:get(http_server, no_server_ref, NodeMsg),
cowboy:get_env(ServerRef, node_msg, no_node_msg).-module(hb_http_server_get_opts_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
get_opts_returns_map_test() ->
Wallet = ar_wallet:new(),
_Node = hb_http_server:start_node(#{priv_wallet => Wallet}),
Opts = hb_http_server:get_opts(#{
http_server => hb_util:human_id(ar_wallet:to_address(Wallet))
}),
?assert(is_map(Opts)).
get_opts_contains_wallet_test() ->
Wallet = ar_wallet:new(),
_Node = hb_http_server:start_node(#{priv_wallet => Wallet}),
Opts = hb_http_server:get_opts(#{
http_server => hb_util:human_id(ar_wallet:to_address(Wallet))
}),
?assert(maps:is_key(priv_wallet, Opts)).
get_opts_contains_port_test() ->
Wallet = ar_wallet:new(),
_Node = hb_http_server:start_node(#{priv_wallet => Wallet}),
Opts = hb_http_server:get_opts(#{
http_server => hb_util:human_id(ar_wallet:to_address(Wallet))
}),
Port = hb_opts:get(port, not_found, Opts),
?assert(is_integer(Port)).
get_opts_contains_store_test() ->
Wallet = ar_wallet:new(),
_Node = hb_http_server:start_node(#{priv_wallet => Wallet}),
Opts = hb_http_server:get_opts(#{
http_server => hb_util:human_id(ar_wallet:to_address(Wallet))
}),
?assert(maps:is_key(store, Opts)).5. set_default_opts/1
-spec set_default_opts(Opts) -> OptsWithDefaults
when
Opts :: map(),
OptsWithDefaults :: map().Description: Apply default values to server options: random port (10000-60000), new wallet, test store.
Defaults:#{
port => RandomPort, % 10000-60000
priv_wallet => NewWallet, % Fresh RSA-4096 keypair
store => [TestStore], % In-memory test store
address => HumanAddress, % Base64url address
force_signed => true % Require signatures
}-module(hb_http_server_set_default_opts_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
set_default_opts_adds_port_test() ->
Opts = hb_http_server:set_default_opts(#{}),
Port = maps:get(port, Opts),
?assert(is_integer(Port)),
?assert(Port >= 10000),
?assert(Port =< 60000).
set_default_opts_adds_wallet_test() ->
Opts = hb_http_server:set_default_opts(#{}),
?assert(maps:is_key(priv_wallet, Opts)).
set_default_opts_adds_store_test() ->
Opts = hb_http_server:set_default_opts(#{}),
?assert(maps:is_key(store, Opts)).
set_default_opts_adds_address_test() ->
Opts = hb_http_server:set_default_opts(#{}),
Address = maps:get(address, Opts),
?assert(is_binary(Address)).
set_default_opts_preserves_passed_port_test() ->
Opts = hb_http_server:set_default_opts(#{port => 9999}),
?assertEqual(9999, maps:get(port, Opts)).
set_default_opts_preserves_passed_wallet_test() ->
Wallet = ar_wallet:new(),
Opts = hb_http_server:set_default_opts(#{priv_wallet => Wallet}),
?assertEqual(Wallet, maps:get(priv_wallet, Opts)).
set_default_opts_force_signed_test() ->
Opts = hb_http_server:set_default_opts(#{}),
?assertEqual(true, maps:get(force_signed, Opts)).6. set_proc_server_id/1
-spec set_proc_server_id(ServerID) -> ok
when
ServerID :: binary().Description: Store server ID in process dictionary for current process.
Implementation:set_proc_server_id(ServerID) ->
put(server_id, ServerID).-module(hb_http_server_proc_id_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
set_proc_server_id_test() ->
ServerID = <<"test-server-id">>,
hb_http_server:set_proc_server_id(ServerID),
?assertEqual(ServerID, get(server_id)).
set_proc_server_id_overwrites_test() ->
hb_http_server:set_proc_server_id(<<"first">>),
hb_http_server:set_proc_server_id(<<"second">>),
?assertEqual(<<"second">>, get(server_id)).
set_proc_server_id_binary_test() ->
Wallet = ar_wallet:new(),
ServerID = hb_util:human_id(ar_wallet:to_address(Wallet)),
hb_http_server:set_proc_server_id(ServerID),
?assert(is_binary(get(server_id))).7. init/2 (Cowboy Callback)
-spec init(Req, ServerID) -> {ok, Req, State}
when
Req :: cowboy_req:req(),
ServerID :: binary(),
State :: term().Description: Handle incoming HTTP request. Marshals request to message, resolves through AO-Core, handles errors, and sends response.
Request Flow:Cowboy Request
↓
Set Process Server ID
↓
Get Node Configuration
↓
Convert to TABM Singleton Messages
↓
Resolve Through AO-Core
↓
Handle Success/Error
↓
Convert to HTTP Response
↓
Send to Clienttry
Messages = hb_http:req_to_tabm_singleton(Req, undefined, NodeMsg),
Results = hb_ao:resolve_many(Messages, NodeMsg),
case dev_monitor:get_error(Results) of
no_error ->
hb_http:reply(Req, lists:last(Results), <<>>, NodeMsg);
ErrorMsg ->
format_error_response(Req, ErrorMsg, NodeMsg)
end
catch
Type:Error:Stack ->
format_error_response(Req, {Type, Error, Stack}, NodeMsg)
end.-module(hb_http_server_init_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
init_handles_get_request_test() ->
Node = hb_http_server:start_node(#{}),
{ok, Response} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
?assert(is_map(Response)).
init_handles_path_request_test() ->
Node = hb_http_server:start_node(#{}),
{ok, Response} = hb_http:get(Node, <<"/~meta@1.0/info/port">>, #{}),
?assert(is_integer(Response)).
init_sets_server_id_test() ->
Wallet = ar_wallet:new(),
Node = hb_http_server:start_node(#{priv_wallet => Wallet}),
{ok, Info} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
Address = hb_ao:get(<<"address">>, Info, #{}),
?assert(is_binary(Address)).
init_returns_node_config_test() ->
Node = hb_http_server:start_node(#{<<"custom-key">> => <<"custom-value">>}),
{ok, Info} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
?assertEqual(<<"custom-value">>, hb_ao:get(<<"custom-key">>, Info, #{})).8. allowed_methods/2 (Cowboy Callback)
-spec allowed_methods(Req, State) -> {[Method], Req, State}
when
Req :: cowboy_req:req(),
State :: term(),
Method :: binary().Description: Return list of allowed HTTP methods.
Implementation:allowed_methods(Req, State) ->
{[<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>, <<"PATCH">>], Req, State}.-module(hb_http_server_methods_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
allowed_methods_test() ->
MockReq = #{},
MockState = <<"test-state">>,
{Methods, _Req, _State} = hb_http_server:allowed_methods(MockReq, MockState),
?assert(is_list(Methods)),
?assert(lists:member(<<"GET">>, Methods)),
?assert(lists:member(<<"POST">>, Methods)),
?assert(lists:member(<<"PUT">>, Methods)),
?assert(lists:member(<<"DELETE">>, Methods)),
?assert(lists:member(<<"OPTIONS">>, Methods)),
?assert(lists:member(<<"PATCH">>, Methods)).
allowed_methods_returns_six_test() ->
{Methods, _, _} = hb_http_server:allowed_methods(#{}, #{}),
?assertEqual(6, length(Methods)).
allowed_methods_preserves_state_test() ->
MockState = <<"my-state">>,
{_Methods, _Req, ReturnedState} = hb_http_server:allowed_methods(#{}, MockState),
?assertEqual(MockState, ReturnedState).Server Configuration
Configuration File Format
Default location: config.flat
#{
% Network
port => 8734,
host => <<"localhost">>,
% Security
priv_key_location => <<"hyperbeam-key.json">>,
force_signed => true,
% Storage
store => [
#{type => rocksdb, path => <<"data/rocksdb">>}
],
store_defaults => #{
create_if_missing => true
},
% Monitoring
prometheus => true,
% Timeouts
idle_timeout => 300000, % 5 minutes
% Protocol
protocol => http2 | http3,
% Hooks
on => #{
<<"start">> => StartHookMessage
}
}.Startup Hooks
Start Hook Execution
HookMsg = #{<<"body">> => NodeMsg},
Result = dev_hook:on(<<"start">>, HookMsg, NodeMsg),
{ok, #{<<"body">> := ModifiedNodeMsg}} = Result.- Modify server configuration dynamically
- Initialize custom devices
- Set up monitoring
- Configure routing
set_node_opts_test() ->
Node = hb_http_server:start_node(#{
on => #{
<<"start">> => #{
<<"device">> => #{
<<"start">> => fun(_, #{<<"body">> := NodeMsg}, _) ->
{ok, #{<<"body">> => NodeMsg#{<<"test-success">> => true}}}
end
}
}
}
}),
{ok, LiveOpts} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}),
?assert(hb_ao:get(<<"test-success">>, LiveOpts, false, #{})).Greeting Banner
ASCII Art Display
===========================================================
== ██╗ ██╗██╗ ██╗██████╗ ███████╗██████╗ ==
== ██║ ██║╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗ ==
== ███████║ ╚████╔╝ ██████╔╝█████╗ ██████╔╝ ==
== ██╔══██║ ╚██╔╝ ██╔═══╝ ██╔══╝ ██╔══██╗ ==
== ██║ ██║ ██║ ██║ ███████╗██║ ██║ ==
== ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ==
== ==
== ██████╗ ███████╗ █████╗ ███╗ ███╗ VERSION: ==
== ██╔══██╗██╔════╝██╔══██╗████╗ ████║ v1.0. ==
== ██████╔╝█████╗ ███████║██╔████╔██║ ==
== ██╔══██╗██╔══╝ ██╔══██║██║╚██╔╝██║ EAT GLASS, ==
== ██████╔╝███████╗██║ ██║██║ ╚═╝ ██║ BUILD THE ==
== ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ FUTURE. ==
===========================================================
== Node activate at: http://localhost:8734 ==
== Operator: xABCD...1234 ==
===========================================================
== Config: ==
===========================================================
#{port => 8734, ...}
===========================================================Suppression: Automatically disabled in test mode
Error Handling
Error Response Format
format_error_response(Req, ErrorMsg, NodeMsg) ->
Singleton = #{
<<"status">> => 500,
<<"status-reason">> => <<"Internal Server Error">>,
<<"content-type">> => <<"text/plain">>
},
FormattedErrorMsg = iolist_to_binary([
"Error: ", hb_util:bin(ErrorMsg), "\n",
"Path: ", cowboy_req:path(Req), "\n",
"Method: ", cowboy_req:method(Req)
]),
hb_http:reply(Req, Singleton, FormattedErrorMsg, NodeMsg).{error, Reason}- Standard errors{throw, Exception}- Exceptions{exit, Reason}- Process exits{Type, Error, Stack}- Catches with stack trace
Server Restart
Dynamic Server Updates
% Start first server
Node1 = hb_http_server:start_node(#{
<<"key">> => <<"value1">>,
priv_wallet => Wallet
}),
% Restart with same wallet (updates configuration)
Node2 = hb_http_server:start_node(#{
<<"key">> => <<"value2">>,
priv_wallet => Wallet
}),
% Node2 URL reflects updated configuration
{ok, <<"value2">>} = hb_http:get(Node2, <<"/~meta@1.0/info/key">>, #{}).restart_server_test() ->
Wallet = ar_wallet:new(),
BaseOpts = #{
<<"test-key">> => <<"server-1">>,
priv_wallet => Wallet,
protocol => http2
},
_Node1 = hb_http_server:start_node(BaseOpts),
Node2 = hb_http_server:start_node(BaseOpts#{<<"test-key">> => <<"server-2">>}),
?assertEqual(
{ok, <<"server-2">>},
hb_http:get(Node2, <<"/~meta@1.0/info/test-key">>, #{protocol => http2})
).Common Patterns
%% Start production server
{ok, _} = hb_http_server:start().
%% Start with custom configuration
{ok, _} = hb_http_server:start(#{
port => 9000,
store => [#{type => rocksdb, path => <<"data">>}],
priv_key_location => <<"keys/node.json">>
}).
%% Start test node
Node = hb_http_server:start_node(),
{ok, Info} = hb_http:get(Node, <<"/~meta@1.0/info">>, #{}).
%% Dynamic configuration update
Opts = hb_http_server:get_opts(),
{ok, NewOpts} = hb_http_server:set_opts(#{<<"new-key">> => <<"value">>}, Opts).
%% Get current server configuration
Config = hb_http_server:get_opts(),
Port = hb_opts:get(port, 8734, Config).
%% Use startup hook
Node = hb_http_server:start_node(#{
on => #{
<<"start">> => #{
<<"device">> => #{
<<"start">> => fun(_, #{<<"body">> := Msg}, _) ->
io:format("Server starting with config: ~p~n", [Msg]),
{ok, #{<<"body">> => Msg}}
end
}
}
}
}).Node History Tracking
% Initial state
Opts1 = get_opts(),
History1 = hb_opts:get(node_history, [], Opts1),
% []
% First update
{ok, Opts2} = set_opts(#{<<"key1">> => <<"val1">>}, Opts1),
History2 = hb_opts:get(node_history, [], Opts2),
% [#{<<"key1">> => <<"val1">>}]
% Second update
{ok, Opts3} = set_opts(#{<<"key2">> => <<"val2">>}, Opts2),
History3 = hb_opts:get(node_history, [], Opts3),
% [#{<<"key1">> => <<"val1">>}, #{<<"key2">> => <<"val2">>}]Protocol Support
HTTP/2 (Default)
ProtoOpts = #{
protocols => [http2],
transport => tcp
}.HTTP/3 (QUIC)
ProtoOpts = #{
protocols => [http3],
transport => quicer
}.#{
protocol => http3,
port => 8734
}Prometheus Integration
Metrics Collection
ProtoOpts = #{
metrics_callback => fun prometheus_cowboy2_instrumenter:observe/1,
stream_handlers => [cowboy_metrics_h, cowboy_stream_h]
}- Request count by method/path
- Response time distribution
- Status code distribution
- Connection count
- Bytes sent/received
Disabled in Tests: Automatically disabled when hb_features:test() returns true
Configuration Precedence
- Command Line/Explicit: Options passed to
start/1 - Configuration File: Loaded from
config.flat - Environment Defaults: From
hb_opts:default_message_with_env() - Hardcoded Defaults: In
set_default_opts/1
References
- HTTP Client -
hb_http.erl - AO-Core -
hb_ao.erl,hb_singleton.erl - Message Handling -
hb_message.erl - Options -
hb_opts.erl - Store -
hb_store.erl - Hooks -
dev_hook.erl
Notes
- Cowboy Integration: Uses Cowboy as HTTP server framework
- Dynamic Configuration: Server options can be updated at runtime
- History Tracking: Maintains history of configuration changes
- Startup Hooks: Allows modification of configuration before server starts
- Test Mode: Greeting banner and Prometheus disabled in tests
- Random Ports: Test nodes use random ports (10000-60000)
- Default Security:
force_signed => truefor test nodes - Store Initialization: Automatic test store creation if not provided
- Wallet Management: Loads from file or creates new for tests
- Server Restart: Same wallet allows configuration updates
- Protocol Choice: HTTP/2 default, HTTP/3 optional
- CORS Support: Handled by
hb_http:reply/4 - Error Formatting: Detailed error messages with stack traces
- Process Dictionary: Server ID stored per-process
- Environment Storage: Configuration stored in Cowboy environment