Skip to content

Subsystems & Custom Features Guide

[!TIP] Complete System Development Tutorial: If you are looking for an end-to-end guide covering architecture, modular development workflows, and a full e-commerce practical walkthrough, please refer to the new System Development Tutorial Overview.

Foundry empowers developers to build modular, maintainable backend applications by organizing business capabilities into Subsystems and Lifecycle Hooks.


A Subsystem is a self-contained business domain living inside your application (e.g. src/systems/blog/ or src/systems/billing/).

src/systems/blog/
├── mod.rs # SubsystemModule trait implementation
├── controllers/ # Axum HTTP handlers mounted at /api/v1/s/blog/ext/*
│ └── mod.rs
├── logic/ # Pure domain services with database access
│ └── mod.rs
├── dto/ # Request/response structs and validator rules
│ └── mod.rs
└── custom_pages/ # Custom Admin UI views (HTML, React, Vue)
└── article_editor.html

2. Implementing a Subsystem (Three-Layer Architecture)

Section titled “2. Implementing a Subsystem (Three-Layer Architecture)”

Step 1: Define DTOs & Validation (dto/mod.rs)

Section titled “Step 1: Define DTOs & Validation (dto/mod.rs)”
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Debug, Clone, Deserialize, Validate)]
pub struct CreateArticleRequest {
#[validate(length(min = 3, max = 120, message = "Title must be 3-120 characters"))]
pub title: String,
#[validate(length(min = 10, message = "Content must be at least 10 characters"))]
pub content: String,
pub author: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ArticleResponse {
pub id: i64,
pub title: String,
pub content: String,
pub author: String,
}

Step 2: Implement Domain Business Logic with Database Access (logic/mod.rs)

Section titled “Step 2: Implement Domain Business Logic with Database Access (logic/mod.rs)”
use crate::systems::blog::dto::{CreateArticleRequest, ArticleResponse};
use foundry::prelude::*;
pub struct ArticleService;
impl ArticleService {
pub async fn create_article(
ctx: &SystemContext,
db: &Database,
req: CreateArticleRequest,
) -> AppResult<ArticleResponse> {
// Multi-database provider-agnostic dynamic model persistence
let record = db.records().create(
&ctx.system_slug,
"articles",
serde_json::json!({
"title": req.title,
"content": req.content,
"author": req.author
})
).await?;
Ok(ArticleResponse {
id: record.id,
title: req.title,
content: req.content,
author: req.author,
})
}
}

Step 3: Implement HTTP Controller with Database Facade (controllers/mod.rs)

Section titled “Step 3: Implement HTTP Controller with Database Facade (controllers/mod.rs)”
use axum::{extract::Extension, routing::post, Json, Router};
use foundry::prelude::*;
use validator::Validate;
use crate::systems::blog::dto::{CreateArticleRequest, ArticleResponse};
use crate::systems::blog::logic::ArticleService;
pub fn build_routes() -> Router {
Router::new().route("/articles", post(handle_create_article))
}
pub async fn handle_create_article(
Extension(ctx): Extension<SystemContext>,
Extension(db): Extension<Database>,
Json(payload): Json<CreateArticleRequest>,
) -> AppResult<Json<ApiResponse<ArticleResponse>>> {
payload.validate()?;
let article = ArticleService::create_article(&ctx, &db, payload).await?;
Ok(Json(ApiResponse::created(article)))
}

Step 4: Implement SubsystemModule (mod.rs)

Section titled “Step 4: Implement SubsystemModule (mod.rs)”
pub mod controllers;
pub mod dto;
pub mod logic;
use foundry::prelude::*;
use std::path::PathBuf;
pub struct BlogSubsystem;
impl SubsystemModule for BlogSubsystem {
fn slug(&self) -> &'static str {
"blog"
}
fn display_name(&self) -> &'static str {
"Blog & Content Publishing"
}
fn description(&self) -> &'static str {
"Article authoring, tag categorization, and publishing pipeline"
}
fn register_routes(&self, router: Router) -> Router {
let mut r = router.merge(controllers::build_routes());
// Serve custom Admin UI pages under /custom-pages
let custom_dir = PathBuf::from("src/systems/blog/custom_pages");
if custom_dir.exists() {
r = r.nest_service("/custom-pages", ServeDir::new(custom_dir));
}
r
}
fn custom_admin_pages(&self) -> Vec<CustomAdminPageSpec> {
vec![CustomAdminPageSpec {
key: "article_editor".to_string(),
title: "Article Studio".to_string(),
icon: "FileEdit".to_string(),
page_type: "iframe".to_string(),
entry: "/api/v1/s/blog/ext/custom-pages/article_editor.html".to_string(),
required_role: None, // e.g. Some("super_admin".to_string())
}]
}
}

In your application’s entry point:

use foundry::prelude::*;
use systems::BlogSubsystem;
#[foundry::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = FoundryApp::builder()
.register_subsystem(BlogSubsystem)
.build()
.await?;
app.run().await?;
Ok(())
}

Custom subsystem pages embedded in the Foundry Admin Shell automatically receive authentication context and theme events via window.addEventListener('message', ...):

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Article Studio</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100 min-h-screen p-6">
<div class="max-w-4xl mx-auto space-y-4">
<h1 class="text-xl font-bold text-indigo-600 dark:text-indigo-400">📝 Article Studio</h1>
<p id="admin-badge" class="text-xs text-slate-500">Connecting to Foundry Admin Shell...</p>
</div>
<script>
let authContext = null;
// 1. Receive context from Foundry Admin Shell
window.addEventListener('message', function(event) {
if (event.data?.type === 'FOUNDRY_INIT') {
authContext = event.data.payload;
if (authContext.theme === 'dark') document.documentElement.classList.add('dark');
document.getElementById('admin-badge').innerText =
`Authenticated as: ${authContext.admin.username} (${authContext.admin.role}) for subsystem: ${authContext.subsystemSlug}`;
}
});
// 2. Send toast notifications back to parent Admin Shell
function showToast(message, level = 'success') {
window.parent.postMessage({
type: 'FOUNDRY_TOAST',
payload: { message, level }
}, '*');
}
</script>
</body>
</html>

Intercept dynamic model mutations through the MutationHook trait:

use foundry::prelude::*;
use serde_json::Value;
pub struct AuditHook;
#[async_trait]
impl MutationHook for AuditHook {
async fn before_create(
&self,
ctx: &SystemContext,
model_slug: &str,
data: &mut Value,
) -> AppResult<()> {
tracing::info!("Creating record in model '{}' (system: {})", model_slug, ctx.system_slug);
Ok(())
}
async fn after_create(
&self,
ctx: &SystemContext,
model_slug: &str,
record_id: i64,
_data: &Value,
) -> AppResult<()> {
tracing::info!("Created record {} in '{}'", record_id, model_slug);
Ok(())
}
}

Register hooks in your builder:

let app = FoundryApp::builder()
.register_hook(AuditHook)
.build()
.await?;