Build the First Application
This tutorial follows the maintained backend template shape. The goal is not to build a product. The goal is to see where AppCore ends and the application begins.
The mental model is simple: business code declares behavior, manifests declare the contract, and deployment chooses the runtime environment.
1. What is the smallest useful business implementation?
The smallest useful application declares a command and registers a handler:
use appcore_bin::application::{
Application, CommandBus, CommandEnvelope, CommandHandler, CommandName,
CommandRegistry, CommandResult, RuntimeContext, RuntimeResult,
};
struct BackendApplication;
impl Application for BackendApplication {
fn register_commands(&self, registry: &mut CommandRegistry) -> RuntimeResult<()> {
registry.register(CommandName::new("app.ping")?)
}
fn register_handlers(&self, bus: &mut CommandBus) -> RuntimeResult<()> {
bus.register_handler(PingHandler)
}
}
struct PingHandler;
impl CommandHandler for PingHandler {
fn command_name(&self) -> CommandName {
CommandName::new("app.ping").expect("static command name")
}
fn handle(
&self,
_command: &CommandEnvelope,
_context: &dyn RuntimeContext,
) -> RuntimeResult<CommandResult> {
Ok(CommandResult::accepted(Vec::new()))
}
}
2. Why should main stay small?
fn main() {
if let Err(error) = appcore_bin::application::run_application(&BackendApplication) {
eprintln!("application failed: {error}");
std::process::exit(1);
}
}
If main starts HTTP servers, opens storage roots, or creates token providers, the application has crossed the runtime boundary.
3. Why must the manifest declare the command too?
[[capabilities]]
id = "app.ping"
version = "1"
mode = "command"
visibility = "local"
requires_leader = false
idempotency_required = true
Because idempotency is required, a command request without an idempotency key is rejected before handler execution.
4. What does the first deployment choose?
manifest_version = 1
installation_id = "backend-template-local"
application_id = "backend-template"
mode = "standalone"
secrets = { runtime_security = "env:APPCORE_BACKEND_TEMPLATE_SECRET" }
paths = { storage = "target/runtime/storage", backup = "target/runtime/backups" }
[storage]
provider_id = "file"
settings = {}
secret_refs = {}
[network]
listen_addresses = ["127.0.0.1:39300"]
peer_transport = "http"
command_transport = "http"
The deployment selects the file storage provider and listener address. The application code does not change if a later deployment selects cluster mode and a different provider set.
5. What should you inspect after it boots?
Set a local secret through the environment, then run the application. The runtime exposes health and status endpoints when the deployment enables a listener.
export APPCORE_BACKEND_TEMPLATE_SECRET="$(openssl rand -hex 32)"
cargo run --manifest-path templates/appcore-backend/Cargo.toml
curl -s http://127.0.0.1:39300/health
curl -s http://127.0.0.1:39300/status
What should you test next?
- manifest mismatch fails bootstrap;
- undeclared command is rejected;
- missing idempotency key is rejected for the declared command;
- storage root rejects traversal paths;
- shutdown moves lifecycle to stopped;
- enabling automatic updates without the managed supervisor path is rejected.
Limitations
- This tutorial intentionally uses a tiny command; it does not model a full domain workflow.
- It assumes the backend template shape and public
appcore_bin::applicationfacade. - It demonstrates standalone local deployment, not a complete clustered installation.
- It does not cover custom provider authoring or production secret management.