Files
archy/docs/developer-guide.md
Dorian a0a7aadcb3
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 12m25s
chore: Debian 12 → 13 (Trixie) migration, service hardening
- Update all references from Debian 12 (Bookworm) to Debian 13 (Trixie)
- Enable SystemCallArchitectures, RestrictAddressFamilies, RestrictRealtime
  in archipelago.service (safe on systemd 256+ which respects NoNewPrivileges=no)
- Update GLIBC compatibility checks from 2.36 to 2.40
- ISO filename, build container, and docs updated throughout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 21:32:08 +02:00

9.8 KiB

Archipelago Developer Guide

Project Structure

archy/
├── core/                          # Rust backend
│   └── archipelago/
│       ├── src/
│       │   ├── main.rs            # Entry point, module declarations
│       │   ├── api/rpc/           # RPC endpoint handlers
│       │   │   ├── mod.rs         # Route dispatcher
│       │   │   ├── auth.rs        # Login, session, TOTP
│       │   │   ├── container.rs   # Container lifecycle
│       │   │   ├── package.rs     # Package install/remove
│       │   │   ├── interfaces.rs  # Network interfaces, WiFi, DNS
│       │   │   ├── federation.rs  # Federation management
│       │   │   ├── marketplace.rs # Community marketplace
│       │   │   └── ...            # Other endpoint groups
│       │   ├── auth.rs            # Password hashing, sessions
│       │   ├── config.rs          # Configuration loading
│       │   ├── server.rs          # HTTP/WS server (axum)
│       │   ├── container/         # Podman integration
│       │   ├── network/           # Network management
│       │   │   ├── dns.rs         # DNS configuration
│       │   │   ├── router.rs      # UPnP, diagnostics
│       │   │   └── dwn_*.rs       # DWN protocol
│       │   ├── federation.rs      # Federation protocol
│       │   ├── marketplace.rs     # Marketplace discovery
│       │   ├── identity.rs        # DID key management
│       │   ├── vpn.rs             # VPN (Tailscale/WireGuard)
│       │   ├── mesh.rs            # Meshtastic mesh networking
│       │   └── ...
│       ├── Cargo.toml
│       └── tests/                 # Integration tests
├── neode-ui/                      # Vue 3 frontend
│   ├── src/
│   │   ├── api/                   # RPC client, WebSocket, container client
│   │   │   └── rpc-client.ts      # Central RPC client (all backend calls)
│   │   ├── views/                 # Page components
│   │   │   ├── Home.vue           # Dashboard with system stats
│   │   │   ├── Marketplace.vue    # App store (curated + community)
│   │   │   ├── Server.vue         # Network, VPN, DNS management
│   │   │   ├── Federation.vue     # Federation dashboard
│   │   │   ├── Settings.vue       # User settings
│   │   │   ├── Web5.vue           # DID, DWN, Nostr
│   │   │   └── ...
│   │   ├── stores/                # Pinia state management
│   │   ├── components/            # Reusable UI components
│   │   ├── composables/           # Vue composables
│   │   ├── router/                # Vue Router with guards
│   │   ├── types/                 # TypeScript type definitions
│   │   └── style.css              # Global styles + Tailwind utilities
│   ├── vite.config.ts
│   └── package.json
├── scripts/                       # Deployment and utility scripts
│   ├── deploy-to-target.sh        # Main deploy script
│   ├── first-boot-containers.sh   # ISO first-boot setup
│   └── run-tests.sh               # CI test runner
├── image-recipe/                  # ISO build configuration
│   ├── build-auto-installer-iso.sh
│   └── configs/                   # Nginx, systemd configs
├── docs/                          # Documentation
│   ├── architecture.md
│   ├── app-manifest-spec.md
│   ├── marketplace-protocol.md
│   └── multi-node-architecture.md
├── apps/                          # App manifests (YAML)
├── CLAUDE.md                      # AI development instructions
└── loop/plan.md                   # Project roadmap

Development Setup

Prerequisites

  • macOS (development machine): Node.js 20+, npm
  • Linux server (192.168.1.228): Rust toolchain, Podman, Nginx, Debian 13
  • SSH key: ~/.ssh/archipelago-deploy

Local Frontend Development

cd neode-ui
npm install
npm start         # Vite dev server on :8100, mock backend on :5959

The dev server at http://localhost:8100 uses a mock backend. Login with password123.

Deploying Changes

Never build Rust on macOS. The deploy script rsyncs source to the Linux server and builds there.

# Deploy to live server (builds backend + frontend, restarts services)
./scripts/deploy-to-target.sh --live

# Deploy to both servers
./scripts/deploy-to-target.sh --both

The deploy script:

  1. Rsyncs source to the server
  2. Builds Rust backend on the server (cargo build --release)
  3. Builds Vue frontend (npm run build)
  4. Copies artifacts to production paths
  5. Restarts the archipelago systemd service
  6. Runs a health check

Running Tests

# Frontend tests
cd neode-ui && npm test

# Backend tests (on dev server via SSH)
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \
  "cd ~/archy/core && cargo test --all-features"

# Both
./scripts/run-tests.sh

Adding a New RPC Endpoint

1. Create the Handler

Add a handler method in the appropriate file under core/archipelago/src/api/rpc/. If no existing file fits, create a new one.

// core/archipelago/src/api/rpc/mymodule.rs
use super::RpcHandler;
use anyhow::Result;

impl RpcHandler {
    /// mymodule.action — description of what it does.
    pub(super) async fn handle_mymodule_action(
        &self,
        params: Option<serde_json::Value>,
    ) -> Result<serde_json::Value> {
        let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
        let name = params
            .get("name")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: name"))?;

        // Your logic here
        let result = do_something(name).await?;

        Ok(serde_json::json!({ "ok": true, "result": result }))
    }
}

Key patterns:

  • Handlers are pub(super) — visible only to the RPC router
  • Accept Option<serde_json::Value> for params (omit for parameterless endpoints)
  • Return Result<serde_json::Value>
  • Use self.config.data_dir for data persistence
  • Use anyhow::bail!() for error responses

2. Register the Route

Add the module declaration and route in core/archipelago/src/api/rpc/mod.rs:

// At the top:
mod mymodule;

// In the match statement (handle_rpc_call):
"mymodule.action" => self.handle_mymodule_action(params).await,

3. Add Module (if new)

If your logic warrants a separate module:

// core/archipelago/src/main.rs
mod mymodule;  // Add to module declarations

4. Frontend Client

Add a convenience method to neode-ui/src/api/rpc-client.ts:

async myAction(params: { name: string }): Promise<{ ok: boolean; result: string }> {
  return this.call({
    method: 'mymodule.action',
    params,
  })
}

5. Deploy and Test

./scripts/deploy-to-target.sh --live
curl -X POST http://192.168.1.228/rpc/v1 \
  -H "Content-Type: application/json" \
  -b "archipelago_session=YOUR_SESSION" \
  -d '{"method":"mymodule.action","params":{"name":"test"}}'

Adding a New Vue Page

1. Create the Component

<!-- neode-ui/src/views/MyPage.vue -->
<template>
  <div>
    <h1 class="text-4xl font-bold text-white mb-2">My Page</h1>
    <div class="glass-card p-6">
      <!-- Content here -->
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { rpcClient } from '@/api/rpc-client'

// State and logic
</script>

2. Add the Route

In neode-ui/src/router/index.ts, add inside the dashboard children:

{
  path: 'my-page',
  name: 'my-page',
  component: () => import('@/views/MyPage.vue'),
},

3. Standards

  • Always use <script setup lang="ts"> — never Options API
  • Use glass-card for containers, bg-white/5 rounded-lg for sub-rows
  • Create global CSS classes in src/style.css instead of inline Tailwind
  • Use rpcClient from @/api/rpc-client.ts for all backend calls
  • Handle loading states and errors for all async operations

Writing Tests

Frontend (Vitest)

// neode-ui/src/api/__tests__/my-test.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'

describe('MyFeature', () => {
  beforeEach(() => {
    vi.restoreAllMocks()
  })

  it('should do something', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ result: 'ok' }),
    }))

    // Test your logic
    expect(true).toBe(true)
  })
})

Backend (Rust)

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[tokio::test]
    async fn test_my_function() {
        let dir = tempdir().unwrap();
        let result = my_function(dir.path()).await.unwrap();
        assert_eq!(result, expected);
    }
}

Code Quality Checklist

  • TypeScript strict mode: no any, use unknown or proper types
  • No unwrap() or expect() in production Rust code — use ?
  • No console.log — wrap in if (import.meta.env.DEV)
  • No empty catch blocks — log or handle errors
  • Functions under 50 lines
  • cargo clippy and cargo fmt pass
  • npx vue-tsc --noEmit passes
  • Security: validate all inputs, no command injection
  • Container security: readonly_root, no_new_privileges, non-root user

Contributing

  1. Create a feature branch: git checkout -b feature/my-feature
  2. Make changes following the standards above
  3. Test locally: cd neode-ui && npm test
  4. Deploy to dev server: ./scripts/deploy-to-target.sh --live
  5. Verify at http://192.168.1.228
  6. Commit with conventional format: feat: add my feature