diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml
index 171a0ce..0e814f6 100644
--- a/crates/server/Cargo.toml
+++ b/crates/server/Cargo.toml
@@ -3,6 +3,10 @@ name = "bichon-server"
version.workspace = true
edition.workspace = true
+[features]
+default = ["embed-web"]
+embed-web = ["dep:rust-embed"]
+
[dependencies]
bichon-core = { path = "../core", features = ["web-api"] }
@@ -17,7 +21,7 @@ poem-openapi = { version = "5.1.16", features = [
"swagger-ui",
"email",
] }
-rust-embed = "8.11.0"
+rust-embed = { version = "8.11.0", optional = true }
email_address.workspace = true
serde.workspace = true
serde_json.workspace = true
diff --git a/crates/server/src/rest/assets.rs b/crates/server/src/rest/assets.rs
index 7675629..bb9590e 100644
--- a/crates/server/src/rest/assets.rs
+++ b/crates/server/src/rest/assets.rs
@@ -1,24 +1,11 @@
-//
-// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
-//
-// This file is part of the Bichon Email Archiving Project
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
+#[cfg(feature = "embed-web")]
+mod inner {
+ use rust_embed::RustEmbed;
+ #[derive(RustEmbed)]
+ #[folder = "../../web/dist/"]
+ pub struct FrontEndAssets;
+}
-use rust_embed::RustEmbed;
-
-#[derive(RustEmbed)]
-#[folder = "../../web/dist/"]
-pub struct FrontEndAssets;
+#[cfg(feature = "embed-web")]
+pub use inner::FrontEndAssets;
diff --git a/crates/server/src/rest/mod.rs b/crates/server/src/rest/mod.rs
index 1349665..468dac3 100644
--- a/crates/server/src/rest/mod.rs
+++ b/crates/server/src/rest/mod.rs
@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+use crate::common::auth::ApiGuard;
use crate::common::error::ErrorCapture;
use crate::common::log::Tracing;
use crate::common::tls::rustls_config;
@@ -29,25 +30,31 @@ use bichon_core::error::BichonResult;
use bichon_core::raise_error;
use bichon_core::settings::cli::SETTINGS;
-use super::error::ApiErrorResponse;
-use crate::common::auth::ApiGuard;
use api::create_openapi_service;
-use assets::FrontEndAssets;
-use http::{HeaderValue, Method};
-use poem::endpoint::EmbeddedFilesEndpoint;
+use http::Method;
use poem::listener::{Listener, TcpListener};
-use poem::middleware::{CatchPanic, Compression, Cors, SetHeader};
-use poem::{get, handler, post, Endpoint, EndpointExt, IntoResponse, Route, Server};
+use poem::middleware::{CatchPanic, Compression, Cors};
+use poem::{get, post, Endpoint, EndpointExt, Route, Server};
use public::oauth2::oauth2_callback;
use std::collections::HashSet;
use std::time::Duration;
+#[cfg(feature = "embed-web")]
+use {
+ assets::FrontEndAssets,
+ http::HeaderValue,
+ poem::{handler, endpoint::EmbeddedFilesEndpoint, IntoResponse},
+ poem::middleware::SetHeader,
+};
+
pub mod api;
pub mod assets;
pub mod public;
pub type ApiResult = std::result::Result;
+use super::error::ApiErrorResponse;
+
/// Build the community route tree. Pro/Enterprise servers can call this
/// and then add their own routes before passing the tree to the server.
pub fn build_routes() -> impl Endpoint {
@@ -95,13 +102,6 @@ pub fn build_routes() -> impl Endpoint {
.expose_headers(vec!["Accept"])
.max_age(SETTINGS.bichon_cors_max_age);
- let cache_static = || {
- SetHeader::new().overriding(
- http::header::CACHE_CONTROL,
- HeaderValue::from_static("max-age=86400"),
- )
- };
-
let app_logic = Route::new()
.nest("/api-docs/swagger", swagger)
.nest("/api-docs/redoc", redoc)
@@ -112,12 +112,9 @@ pub fn build_routes() -> impl Endpoint {
.nest("/oauth2/callback", get(oauth2_callback))
.nest("/api/status", get(get_status))
.nest("/api/login", post(login))
- .nest_no_strip("/api/v1", open_api_route)
- .nest_no_strip(
- "/assets",
- EmbeddedFilesEndpoint::::new().with(cache_static()),
- )
- .at("/*", serve_index_with_base);
+ .nest_no_strip("/api/v1", open_api_route);
+
+ let app_logic = add_web_assets(app_logic);
Route::new()
.nest(&SETTINGS.bichon_base_url, app_logic)
@@ -126,6 +123,52 @@ pub fn build_routes() -> impl Endpoint {
.with(CatchPanic::new())
}
+#[cfg(feature = "embed-web")]
+fn add_web_assets(route: Route) -> impl Endpoint {
+ let cache_static = || {
+ SetHeader::new().overriding(
+ http::header::CACHE_CONTROL,
+ HeaderValue::from_static("max-age=86400"),
+ )
+ };
+
+ route
+ .nest_no_strip(
+ "/assets",
+ EmbeddedFilesEndpoint::::new().with(cache_static()),
+ )
+ .at("/*", serve_index_with_base)
+}
+
+#[cfg(not(feature = "embed-web"))]
+fn add_web_assets(route: Route) -> Route {
+ route
+}
+
+#[cfg(feature = "embed-web")]
+#[handler]
+async fn serve_index_with_base() -> impl IntoResponse {
+ let mut html =
+ String::from_utf8_lossy(&FrontEndAssets::get("index.html").unwrap().data).to_string();
+
+ let raw_base = &SETTINGS.bichon_base_url;
+ let base_href = if raw_base.ends_with('/') {
+ raw_base.clone()
+ } else {
+ format!("{}/", raw_base)
+ };
+
+ let inject_content = format!(
+ r#""#,
+ base_href, raw_base
+ );
+
+ html = html.replace("", &format!("{}", inject_content));
+ poem::Response::builder()
+ .content_type("text/html; charset=utf-8")
+ .body(html)
+}
+
pub async fn start_http_server() -> BichonResult<()> {
let listener = TcpListener::bind((
SETTINGS.bichon_bind_ip.clone().unwrap_or("0.0.0.0".into()),
@@ -160,26 +203,3 @@ pub async fn start_http_server() -> BichonResult<()> {
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))
}
-
-#[handler]
-async fn serve_index_with_base() -> impl IntoResponse {
- let mut html =
- String::from_utf8_lossy(&FrontEndAssets::get("index.html").unwrap().data).to_string();
-
- let raw_base = &SETTINGS.bichon_base_url;
- let base_href = if raw_base.ends_with('/') {
- raw_base.clone()
- } else {
- format!("{}/", raw_base)
- };
-
- let inject_content = format!(
- r#""#,
- base_href, raw_base
- );
-
- html = html.replace("", &format!("{}", inject_content));
- poem::Response::builder()
- .content_type("text/html; charset=utf-8")
- .body(html)
-}