Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,13 @@ FETCH_WORKERS=10

# Number of blocks to fetch per RPC batch request (reduces HTTP round-trips)
RPC_BATCH_SIZE=20

# Branding / White-label (all optional)
# If not set, the default ev-node branding is used.
CHAIN_NAME=MyChain
CHAIN_LOGO_URL= # URL or path to logo (e.g., /branding/logo.png). Default: bundled ev-node logo
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid whitespace placeholder values for CHAIN_LOGO_URL.

This line currently assigns spaces rather than a clean empty value. Use CHAIN_LOGO_URL= and keep the description in a separate comment.

Proposed fix
-CHAIN_LOGO_URL=                       # URL or path to logo (e.g., /branding/logo.png). Default: bundled ev-node logo
+# URL or path to logo (e.g., /branding/logo.png). Default: bundled ev-node logo
+CHAIN_LOGO_URL=
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CHAIN_LOGO_URL= # URL or path to logo (e.g., /branding/logo.png). Default: bundled ev-node logo
# URL or path to logo (e.g., /branding/logo.png). Default: bundled ev-node logo
CHAIN_LOGO_URL=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 22-22: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 22-22: [UnorderedKey] The CHAIN_LOGO_URL key should go before the CHAIN_NAME key

(UnorderedKey)


[warning] 22-22: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example at line 22, Replace the whitespace placeholder assignment for
CHAIN_LOGO_URL with a clean empty value (use CHAIN_LOGO_URL=) and move the
explanatory text into a commented line (e.g., # URL or path to logo (e.g.,
/branding/logo.png). Default: bundled ev-node logo) so the env var is empty by
default and the description remains for contributors; update the CHAIN_LOGO_URL
line in the example file accordingly.

ACCENT_COLOR=#dc2626 # Primary accent color (links, buttons, active states)
BACKGROUND_COLOR_DARK=#050505 # Dark mode base background
BACKGROUND_COLOR_LIGHT=#f4ede6 # Light mode base background
SUCCESS_COLOR=#22c55e # Success indicator color
ERROR_COLOR=#dc2626 # Error indicator color
Comment on lines +23 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

dotenvy dotenv hex color values # parsing comments quoting

💡 Result:

In Dotenvy, # is treated as a comment only:

  • when it’s on its own line, or
  • when it appears after the end of a value (e.g. SECRET_KEY=abc # comment). (hexdocs.pm)

# is not a comment inside quotes. (hexdocs.pm)

So, to safely store hex colors (and avoid any “inline comment” ambiguity across tools), put them in quotes:

PRIMARY_COLOR="#ff00aa"
ACCENT_COLOR='#09f'

If you leave it unquoted, it generally works as long as you don’t introduce a comment-style # after the value (notably, a # that starts an inline comment). (hexdocs.pm)

Citations:


🌐 Web query:

Docker Compose env-file hex color # symbol parsing

💡 Result:

In Docker Compose .env / env_file files, # is only treated as a comment marker when:

  • the line starts with #, or
  • for inline comments, the # is preceded by at least one space in an unquoted value (e.g., VAR=VAL # comment). Otherwise it’s part of the value (e.g., VAR=VAL#not-comment). [1]

So a hex color is safe as:

COLOR=#ff00aa

But if you add an inline comment, you must quote (or ensure there’s no space before #):

COLOR="#ff00aa" # this is fine
# or
COLOR=#ff00aa#not-a-comment

Also note: if you put a hex color directly in compose.yaml, YAML itself treats # as a comment, so you must quote there too:

environment:
  COLOR: "#ff00aa"

[1]

Sources:
[1] Docker Docs – .env file syntax / comment rules (variable interpolation docs) (docs.docker.com)

Citations:


🌐 Web query:

dotenv best practices quoting values with special characters hash

💡 Result:

Core rule: # often starts a comment unless it’s inside quotes

Many dotenv parsers treat # as the beginning of an inline comment in unquoted values (often when it’s preceded by whitespace). python-dotenv, for example, “interpret[s] # as start of comment only if preceded by whitespace.” [1] Some dotenv syntaxes also spell out the same idea: if the value is not quoted, # must be separated by at least one space to be treated as a comment; otherwise it becomes part of the value. [2]

Best practice: quote any value that may contain #, spaces, or other special characters

If your secret/API key can include #, spaces, or trailing/leading whitespace, wrap it in quotes to avoid truncation or accidental comment parsing. DotenvParser explicitly recommends quoting to “allow usage of # in value.” [3]

Use single quotes when you want the value to be taken literally (no escaping / interpolation in many implementations). Dotenvy notes that single-quoted values “remain as-is.” [4]

Examples

# Safe: # is part of the value because it’s quoted
SECRET_HASH="abc#def"

# Also safe (often most “literal”):
SECRET_HASH='abc#def'

# Risky: many parsers will treat " # ..." as a comment and truncate to "abc"
SECRET_HASH=abc `#def`

Practical guidance checklist

  • If a value might contain #always quote it. [3]
  • If a value includes spaces or you care about leading/trailing whitespacequote it. [3]
  • Prefer single quotes for “no surprises / literal” values when your parser supports them. [4]
  • If you must keep the value unquoted, assume # can become a comment delimiter depending on whitespace rules in your dotenv loader. [1]

Sources: [1] python-dotenv PyPI notes, [2] Nvir dotenv syntax reference, [3] DotenvParser docs, [4] Dotenvy dotenv file format docs.


Quote hex color values for cross-tool compatibility and to avoid linter warnings.

While modern dotenv parsers (Dotenvy, Docker Compose) correctly handle unquoted hex colors like #dc2626, quoting removes ambiguity and is the recommended practice across different dotenv implementations and linters.

Proposed fix
-ACCENT_COLOR=#dc2626                  # Primary accent color (links, buttons, active states)
-BACKGROUND_COLOR_DARK=#050505         # Dark mode base background
-BACKGROUND_COLOR_LIGHT=#f4ede6        # Light mode base background
-SUCCESS_COLOR=#22c55e                 # Success indicator color
-ERROR_COLOR=#dc2626                   # Error indicator color
+ACCENT_COLOR="#dc2626"                  # Primary accent color (links, buttons, active states)
+BACKGROUND_COLOR_DARK="#050505"         # Dark mode base background
+BACKGROUND_COLOR_LIGHT="#f4ede6"        # Light mode base background
+SUCCESS_COLOR="#22c55e"                 # Success indicator color
+ERROR_COLOR="#dc2626"                   # Error indicator color
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ACCENT_COLOR=#dc2626 # Primary accent color (links, buttons, active states)
BACKGROUND_COLOR_DARK=#050505 # Dark mode base background
BACKGROUND_COLOR_LIGHT=#f4ede6 # Light mode base background
SUCCESS_COLOR=#22c55e # Success indicator color
ERROR_COLOR=#dc2626 # Error indicator color
ACCENT_COLOR="#dc2626" # Primary accent color (links, buttons, active states)
BACKGROUND_COLOR_DARK="#050505" # Dark mode base background
BACKGROUND_COLOR_LIGHT="#f4ede6" # Light mode base background
SUCCESS_COLOR="#22c55e" # Success indicator color
ERROR_COLOR="#dc2626" # Error indicator color
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 23-23: [UnorderedKey] The ACCENT_COLOR key should go before the CHAIN_LOGO_URL key

(UnorderedKey)


[warning] 23-23: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 24-24: [UnorderedKey] The BACKGROUND_COLOR_DARK key should go before the CHAIN_LOGO_URL key

(UnorderedKey)


[warning] 24-24: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 25-25: [UnorderedKey] The BACKGROUND_COLOR_LIGHT key should go before the CHAIN_LOGO_URL key

(UnorderedKey)


[warning] 25-25: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 26-26: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 27-27: [UnorderedKey] The ERROR_COLOR key should go before the SUCCESS_COLOR key

(UnorderedKey)


[warning] 27-27: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 23 - 27, Update the .env.example so the hex color
values are quoted to avoid parser/linter issues: wrap the values for
ACCENT_COLOR, BACKGROUND_COLOR_DARK, BACKGROUND_COLOR_LIGHT, SUCCESS_COLOR, and
ERROR_COLOR in quotes (e.g., " #... " or ' #... ') while keeping the existing
inline comments intact; this ensures cross-tool compatibility for dotenv parsers
and linters without changing the variable names or comments.

3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,13 @@ Copy `.env.example` to `.env` and set `RPC_URL`. Common options:
| `IPFS_GATEWAY` | Gateway for NFT metadata | `https://ipfs.io/ipfs/` |
| `REINDEX` | Wipe and reindex from start | `false` |

See [White Labeling](docs/WHITE_LABELING.md) for branding customization (chain name, logo, colors).

## Documentation

- [API Reference](docs/API.md)
- [Architecture](docs/ARCHITECTURE.md)
- [White Labeling](docs/WHITE_LABELING.md)
- [Product Requirements](docs/PRD.md)

## License
Expand Down
36 changes: 36 additions & 0 deletions backend/crates/atlas-api/src/handlers/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use axum::{extract::State, Json};
use serde::Serialize;
use std::sync::Arc;

use crate::AppState;

#[derive(Serialize)]
pub struct BrandingConfig {
pub chain_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accent_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background_color_dark: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background_color_light: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub success_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_color: Option<String>,
}

/// GET /api/config - Returns white-label branding configuration
/// No DB access, no auth — returns static config from environment variables
pub async fn get_config(State(state): State<Arc<AppState>>) -> Json<BrandingConfig> {
Json(BrandingConfig {
chain_name: state.chain_name.clone(),
logo_url: state.chain_logo_url.clone(),
accent_color: state.accent_color.clone(),
background_color_dark: state.background_color_dark.clone(),
background_color_light: state.background_color_light.clone(),
success_color: state.success_color.clone(),
error_color: state.error_color.clone(),
})
}
1 change: 1 addition & 0 deletions backend/crates/atlas-api/src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod addresses;
pub mod auth;
pub mod blocks;
pub mod config;
pub mod contracts;
pub mod etherscan;
pub mod labels;
Expand Down
35 changes: 35 additions & 0 deletions backend/crates/atlas-api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ pub struct AppState {
pub rpc_url: String,
pub solc_path: String,
pub admin_api_key: Option<String>,
// White-label branding
pub chain_name: String,
pub chain_logo_url: Option<String>,
pub accent_color: Option<String>,
pub background_color_dark: Option<String>,
pub background_color_light: Option<String>,
pub success_color: Option<String>,
pub error_color: Option<String>,
}

#[tokio::main]
Expand All @@ -40,6 +48,24 @@ async fn main() -> Result<()> {
let rpc_url = std::env::var("RPC_URL").expect("RPC_URL must be set");
let solc_path = std::env::var("SOLC_PATH").unwrap_or_else(|_| "solc".to_string());
let admin_api_key = std::env::var("ADMIN_API_KEY").ok();
let chain_name = std::env::var("CHAIN_NAME")
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "Unknown".to_string());
let chain_logo_url = std::env::var("CHAIN_LOGO_URL")
.ok()
.filter(|s| !s.is_empty());
let accent_color = std::env::var("ACCENT_COLOR").ok().filter(|s| !s.is_empty());
let background_color_dark = std::env::var("BACKGROUND_COLOR_DARK")
.ok()
.filter(|s| !s.is_empty());
let background_color_light = std::env::var("BACKGROUND_COLOR_LIGHT")
.ok()
.filter(|s| !s.is_empty());
let success_color = std::env::var("SUCCESS_COLOR")
.ok()
.filter(|s| !s.is_empty());
let error_color = std::env::var("ERROR_COLOR").ok().filter(|s| !s.is_empty());
Comment on lines +51 to +68
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add unit tests for the new env-parsing logic in this file.

Branding parsing behavior (unset/empty/whitespace handling and defaults) is new logic and should be covered by a local #[cfg(test)] mod tests.

As per coding guidelines: "Add unit tests for new logic in a #[cfg(test)] mod tests block in the same file and run with cargo test --workspace".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/crates/atlas-api/src/main.rs` around lines 51 - 68, Add a
#[cfg(test)] mod tests block in the same file that exercises the new env-parsing
logic by setting and unsetting environment variables and asserting the resulting
values of chain_name, chain_logo_url, accent_color, background_color_dark,
background_color_light, success_color, and error_color; specifically write tests
for (1) unset vars producing None or the default "Unknown" for chain_name, (2)
empty-string vars treated as missing, and (3) whitespace-only chain_name trimmed
to default, using std::env::set_var and std::env::remove_var to control state
and re-evaluating the parsing code (call the same expressions or extract the
parsing into a small helper you can invoke in tests), and run via cargo test
--workspace.

Comment on lines +55 to +68
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Trim optional branding env vars before filtering empties.

filter(|s| !s.is_empty()) still accepts whitespace-only values (e.g. " "), which can leak invalid logo/color config into the frontend. Normalize with trim() first.

Proposed fix
+    let optional_env = |key: &str| {
+        std::env::var(key)
+            .ok()
+            .map(|s| s.trim().to_string())
+            .filter(|s| !s.is_empty())
+    };
+
-    let chain_logo_url = std::env::var("CHAIN_LOGO_URL")
-        .ok()
-        .filter(|s| !s.is_empty());
-    let accent_color = std::env::var("ACCENT_COLOR").ok().filter(|s| !s.is_empty());
-    let background_color_dark = std::env::var("BACKGROUND_COLOR_DARK")
-        .ok()
-        .filter(|s| !s.is_empty());
-    let background_color_light = std::env::var("BACKGROUND_COLOR_LIGHT")
-        .ok()
-        .filter(|s| !s.is_empty());
-    let success_color = std::env::var("SUCCESS_COLOR")
-        .ok()
-        .filter(|s| !s.is_empty());
-    let error_color = std::env::var("ERROR_COLOR").ok().filter(|s| !s.is_empty());
+    let chain_logo_url = optional_env("CHAIN_LOGO_URL");
+    let accent_color = optional_env("ACCENT_COLOR");
+    let background_color_dark = optional_env("BACKGROUND_COLOR_DARK");
+    let background_color_light = optional_env("BACKGROUND_COLOR_LIGHT");
+    let success_color = optional_env("SUCCESS_COLOR");
+    let error_color = optional_env("ERROR_COLOR");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let chain_logo_url = std::env::var("CHAIN_LOGO_URL")
.ok()
.filter(|s| !s.is_empty());
let accent_color = std::env::var("ACCENT_COLOR").ok().filter(|s| !s.is_empty());
let background_color_dark = std::env::var("BACKGROUND_COLOR_DARK")
.ok()
.filter(|s| !s.is_empty());
let background_color_light = std::env::var("BACKGROUND_COLOR_LIGHT")
.ok()
.filter(|s| !s.is_empty());
let success_color = std::env::var("SUCCESS_COLOR")
.ok()
.filter(|s| !s.is_empty());
let error_color = std::env::var("ERROR_COLOR").ok().filter(|s| !s.is_empty());
let optional_env = |key: &str| {
std::env::var(key)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
};
let chain_logo_url = optional_env("CHAIN_LOGO_URL");
let accent_color = optional_env("ACCENT_COLOR");
let background_color_dark = optional_env("BACKGROUND_COLOR_DARK");
let background_color_light = optional_env("BACKGROUND_COLOR_LIGHT");
let success_color = optional_env("SUCCESS_COLOR");
let error_color = optional_env("ERROR_COLOR");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/crates/atlas-api/src/main.rs` around lines 55 - 68, Env vars for
optional branding (chain_logo_url, accent_color, background_color_dark,
background_color_light, success_color, error_color) should be normalized by
trimming whitespace before empty-checking; update each initializer to trim the
retrieved String (e.g., via mapping to s.trim().to_string() or using filter_map
with s.trim()) and then filter out empty results so whitespace-only values are
treated as empty and not passed to the frontend.

let host = std::env::var("API_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
let port: u16 = std::env::var("API_PORT")
.unwrap_or_else(|_| "3000".to_string())
Expand All @@ -58,6 +84,13 @@ async fn main() -> Result<()> {
rpc_url,
solc_path,
admin_api_key,
chain_name,
chain_logo_url,
accent_color,
background_color_dark,
background_color_light,
success_color,
error_color,
});

// Build router
Expand Down Expand Up @@ -209,6 +242,8 @@ async fn main() -> Result<()> {
.route("/api/search", get(handlers::search::search))
// Status
.route("/api/status", get(handlers::status::get_status))
// Config (white-label branding)
.route("/api/config", get(handlers::config::get_config))
// Health
.route("/health", get(|| async { "OK" }))
.layer(TimeoutLayer::with_status_code(
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ services:
API_HOST: 0.0.0.0
API_PORT: 3000
RUST_LOG: atlas_api=info,tower_http=info
CHAIN_NAME: ${CHAIN_NAME:-Unknown}
CHAIN_LOGO_URL: ${CHAIN_LOGO_URL:-}
ACCENT_COLOR: ${ACCENT_COLOR:-}
BACKGROUND_COLOR_DARK: ${BACKGROUND_COLOR_DARK:-}
BACKGROUND_COLOR_LIGHT: ${BACKGROUND_COLOR_LIGHT:-}
SUCCESS_COLOR: ${SUCCESS_COLOR:-}
ERROR_COLOR: ${ERROR_COLOR:-}
ports:
- "3000:3000"
depends_on:
Expand All @@ -60,6 +67,8 @@ services:
dockerfile: Dockerfile
ports:
- "80:8080"
volumes:
- ${BRANDING_DIR:-./branding}:/usr/share/nginx/html/branding:ro
depends_on:
- atlas-api
restart: unless-stopped
Expand Down
127 changes: 127 additions & 0 deletions docs/WHITE_LABELING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# White Labeling

Atlas supports white-labeling so each L2 chain can customize the explorer's appearance — name, logo, and color scheme — without rebuilding the frontend.

All branding is configured through environment variables. When none are set, the explorer uses the default ev-node branding (red accent, dark/warm-beige backgrounds). `CHAIN_NAME` defaults to "Unknown" — deployers should always set it.

## Configuration

Add these variables to your `.env` file alongside `RPC_URL`:

| Variable | Description | Default (ev-node) |
|----------|-------------|--------------------|
| `CHAIN_NAME` | Displayed in the navbar, page title, and welcome page | `Unknown` |
| `CHAIN_LOGO_URL` | URL or path to your logo (e.g. `/branding/logo.svg`) | Bundled ev-node logo |
| `ACCENT_COLOR` | Primary accent hex for links, buttons, active states | `#dc2626` |
| `BACKGROUND_COLOR_DARK` | Dark mode base background hex | `#050505` |
| `BACKGROUND_COLOR_LIGHT` | Light mode base background hex | `#f4ede6` |
| `SUCCESS_COLOR` | Success indicator hex (e.g. confirmed badges) | `#22c55e` |
| `ERROR_COLOR` | Error indicator hex (e.g. failed badges) | `#dc2626` |

All variables are optional. Unset variables use the ev-node defaults shown above.

## Custom Logo

To use a custom logo, place your image file in a `branding/` directory at the project root and set `CHAIN_LOGO_URL` to its path:

```text
atlas/
├── branding/
│ └── logo.svg # Your custom logo
├── .env
├── docker-compose.yml
└── ...
```

```env
CHAIN_LOGO_URL=/branding/logo.svg
```

The logo appears in the navbar, the welcome page, and as the browser favicon.

### Docker

In Docker, the `branding/` directory is mounted into the frontend container as a read-only volume. This is configured automatically in `docker-compose.yml`:

```yaml
atlas-frontend:
volumes:
- ${BRANDING_DIR:-./branding}:/usr/share/nginx/html/branding:ro
```

To use a different directory, set `BRANDING_DIR` in your `.env`:

```env
BRANDING_DIR=/path/to/my/assets
```

### Local Development

For `bun run dev`, create a symlink so Vite's dev server can serve the branding files:

```bash
cd frontend/public
ln -s ../../branding branding
```

## Color System

### Accent Color

`ACCENT_COLOR` sets the primary interactive color used for links, buttons, focus rings, and active indicators throughout the UI.

### Background Colors

Each theme (dark and light) takes a single base color. The frontend automatically derives a full surface palette from it:

- **5 surface shades** (from darkest to lightest for dark mode, reversed for light mode)
- **Border color**
- **Text hierarchy** (primary, secondary, muted, subtle, faint)

This means you only need to set one color per theme to get a cohesive palette.

### Success and Error Colors

`SUCCESS_COLOR` and `ERROR_COLOR` control status badges and indicators. For example, "Success" transaction badges use the success color, and "Failed" badges use the error color.

## Examples

### Blue theme

```env
CHAIN_NAME=MegaChain
CHAIN_LOGO_URL=/branding/logo.png
ACCENT_COLOR=#3b82f6
BACKGROUND_COLOR_DARK=#0a0a1a
BACKGROUND_COLOR_LIGHT=#e6f0f4
```

### Green theme (Eden)

```env
CHAIN_NAME=Eden
CHAIN_LOGO_URL=/branding/logo.svg
ACCENT_COLOR=#4ade80
BACKGROUND_COLOR_DARK=#0a1f0a
BACKGROUND_COLOR_LIGHT=#e8f5e8
SUCCESS_COLOR=#22c55e
ERROR_COLOR=#dc2626
```

### Minimal — just rename

```env
CHAIN_NAME=MyChain
```

Everything else stays default ev-node branding.

## How It Works

1. The backend reads branding env vars at startup and serves them via `GET /api/config`
2. The frontend fetches this config once on page load
3. CSS custom properties are set on the document root, overriding the defaults
4. Background surface shades are derived automatically using HSL color manipulation
5. The page title, navbar logo, and favicon are updated dynamically

No frontend rebuild is needed — just change the env vars and restart the API.
6 changes: 6 additions & 0 deletions frontend/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ server {
try_files $uri $uri/ /index.html;
}

# Serve mounted branding assets (logos, etc.)
location /branding/ {
alias /usr/share/nginx/html/branding/;
expires 1h;
}

# Proxy API requests to atlas-api service
location /api/ {
proxy_pass http://atlas-api:3000/api/;
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import {
AddressesPage,
} from './pages';
import { ThemeProvider } from './context/ThemeContext';
import { BrandingProvider } from './context/BrandingContext';

export default function App() {
return (
<ThemeProvider>
<BrandingProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
Expand All @@ -43,6 +45,7 @@ export default function App() {
</Route>
</Routes>
</BrowserRouter>
</BrandingProvider>
</ThemeProvider>
);
}
16 changes: 16 additions & 0 deletions frontend/src/api/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import client from './client';

export interface BrandingConfig {
chain_name: string;
logo_url?: string;
accent_color?: string;
background_color_dark?: string;
background_color_light?: string;
success_color?: string;
error_color?: string;
}

export async function getConfig(): Promise<BrandingConfig> {
const response = await client.get<BrandingConfig>('/config');
return response.data;
}
11 changes: 7 additions & 4 deletions frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import SearchBar from './SearchBar';
import useLatestBlockHeight from '../hooks/useLatestBlockHeight';
import SmoothCounter from './SmoothCounter';
import logoImg from '../assets/logo.png';
import defaultLogoImg from '../assets/logo.png';
import { BlockStatsContext } from '../context/BlockStatsContext';
import { useTheme } from '../hooks/useTheme';
import { useBranding } from '../hooks/useBranding';

export default function Layout() {
const location = useLocation();
Expand Down Expand Up @@ -103,6 +104,8 @@ export default function Layout() {
}`;
const { theme, toggleTheme } = useTheme();
const isDark = theme === 'dark';
const { chainName, logoUrl } = useBranding();
const logoSrc = logoUrl || defaultLogoImg;

return (
<div className="min-h-screen flex flex-col">
Expand All @@ -112,8 +115,8 @@ export default function Layout() {
<div className="grid grid-cols-3 items-center h-16">
{/* Logo */}
<div className="flex md:justify-start justify-center">
<Link to="/" className="flex items-center" aria-label="Atlas Home">
<img src={logoImg} alt="Atlas" className="h-12 w-auto rounded-lg" />
<Link to="/" className="flex items-center" aria-label={`${chainName} Home`}>
<img src={logoSrc} alt={chainName} className="h-12 w-auto rounded-lg" />
</Link>
</div>

Expand Down Expand Up @@ -175,7 +178,7 @@ export default function Layout() {
</button>
<div className="flex items-center gap-3 text-sm text-gray-300">
<span
className={`inline-block w-2.5 h-2.5 rounded-full ${recentlyUpdated ? 'bg-red-500 live-dot' : 'bg-gray-600'}`}
className={`inline-block w-2.5 h-2.5 rounded-full ${recentlyUpdated ? 'bg-accent-primary live-dot' : 'bg-gray-600'}`}
title={recentlyUpdated ? 'Live updates' : 'Idle'}
/>
<SmoothCounter value={displayedHeight} />
Expand Down
Loading