Skip to content
/ saas Public

SaaS application

Notifications You must be signed in to change notification settings

a8n-tools/saas

Repository files navigation

example.com

A SaaS platform hosting developer and productivity tools. We sell convenience, reliability, and managed hosting for open-source applications.

Overview

example.com provides hosted versions of open-source developer tools with:

  • No server setup, maintenance, or updates required
  • Managed infrastructure with monitoring and backups
  • Dedicated support for subscribers
  • $3/month flat price for access to all current and future tools
  • Fixed price for life - early adopters lock in their rate forever

Current Applications

Application Description Subdomain
RUS URL shortening with QR codes rus.example.com
Rusty Links Bookmark management rustylinks.example.com

Tech Stack

Layer Technology
Backend Rust, Actix-Web
Frontend React 18+, Vite, TypeScript, Tailwind CSS, shadcn/ui
Database PostgreSQL 16+
Containerization Docker, Docker Compose
Reverse Proxy Traefik
Email Stalwart (self-hosted)
Monitoring Prometheus, Grafana
Error Tracking GlitchTip (self-hosted)

Quick Start

Prerequisites

  • Docker and Docker Compose
  • Rust toolchain (for local development)
  • Bun
  • Git

Setup

  1. Clone the repository:

    git clone https://github.com/your-org/a8n-tools.git
    cd a8n-tools
  2. Copy environment file:

    cp .env.example .env
  3. Start the development environment:

    just dev
  4. Access the applications:

    With Traefik routing:

  5. Add to /etc/hosts (optional, for subdomain routing):

    127.0.0.1 localhost api.localhost admin.localhost
    

Project Structure

a8n-tools/
├── api/                    # Rust backend (Actix-Web)
│   ├── src/
│   │   ├── main.rs         # Entry point
│   │   ├── config.rs       # Configuration loading
│   │   ├── errors.rs       # Error types
│   │   ├── responses.rs    # Response types
│   │   ├── routes/         # Route definitions
│   │   ├── handlers/       # Request handlers
│   │   ├── models/         # Data models
│   │   ├── services/       # Business logic
│   │   └── middleware/     # Custom middleware
│   ├── migrations/         # Database migrations
│   ├── Cargo.toml
│   └── Dockerfile.dev
├── frontend/               # React frontend
│   ├── src/
│   │   ├── api/            # API client
│   │   ├── components/     # UI components
│   │   ├── pages/          # Page components
│   │   ├── hooks/          # Custom hooks
│   │   ├── stores/         # Zustand stores
│   │   ├── lib/            # Utilities
│   │   └── types/          # TypeScript types
│   ├── package.json
│   └── Dockerfile.dev
├── apps/                   # Hosted applications
│   ├── rus/
│   └── rustylinks/
├── traefik/                # Traefik configuration
├── docs/                   # Documentation
├── docker-compose.dev.yml  # Development environment
├── Justfile                # Development commands
└── .env.example            # Environment template

Test Structure

frontend/src/
  ├── test/
  │   ├── setup.ts          # Test setup (jest-dom, MSW server)
  │   ├── utils.tsx          # Custom render with providers
  │   └── mocks/
  │       ├── handlers.ts    # MSW API mock handlers
  │       └── server.ts      # MSW server instance
  ├── api/
  │   └── auth.test.ts       # Auth API tests
  └── stores/
      └── authStore.test.ts  # Auth store tests

Navigate to frontend directory

cd frontend

Run tests in watch mode (re-runs on file changes)

bun test

Run tests once (CI mode)

bun run test:run

Run tests with coverage report

bun run test:coverage

Current Test Coverage

auth.test.ts - 13 tests (login, register, logout, magic link, password reset) authStore.test.ts - 17 tests (state management, login/logout flow, error handling)

Check if migrations are in sync

Run this command if the _sqlx_migrations table was emptied on accident If this returns 0 but tables exist, you know there's a problem before the API crashes.

docker exec a8n-tools-postgres psql -U a8n -d a8n_platform -c \
   "SELECT COUNT(*) FROM _sqlx_migrations;"

Admin Setup

To promote a user to admin, connect to the database and update their role:

just db-shell
UPDATE users
SET role = 'admin'
WHERE email = 'your@email.com';

Once you have an admin account, you can promote additional users from the admin UI at the Users page.

Development

Available Commands

Run just --list to see all available commands:

# Start development environment
just dev

# Stop all services
just down

# View logs
just logs
just logs-api
just logs-frontend

# Database operations
just db-shell       # Connect to PostgreSQL
just migrate        # Run migrations
just migrate-create create_users  # Create new migration

# Testing
just test           # Run all tests
just test-api       # Run API tests only
just test-frontend  # Run frontend tests only

# Build
just build          # Build all Docker images

# Cleanup
just clean          # Stop services and remove volumes

Adding a New API Endpoint

  1. Create a handler in api/src/handlers/
  2. Define the route in api/src/routes/
  3. Register the route in api/src/routes/mod.rs

Example:

// api/src/handlers/users.rs
use actix_web::{get, web, HttpResponse};
use crate::responses::success;

#[get("/users/me")]
async fn get_current_user() -> HttpResponse {
    // Handler implementation
}

Adding a New Frontend Page

  1. Create the page component in frontend/src/pages/
  2. Add the route in frontend/src/App.tsx
  3. Update navigation if needed

Environment Variables

Variable Description Default Required
DATABASE_URL PostgreSQL connection string - Yes
HOST API server host 0.0.0.0 No
PORT API server port 8080 No
RUST_LOG Log level info No
CORS_ORIGIN Allowed CORS origin https://app.example.com No
ENVIRONMENT Environment name development No
JWT_PRIVATE_KEY_PATH Path to Ed25519 private key - Yes (prod)
JWT_PUBLIC_KEY_PATH Path to Ed25519 public key - Yes (prod)
STRIPE_SECRET_KEY Stripe API secret key - Yes (prod)
STRIPE_WEBHOOK_SECRET Stripe webhook signing secret - Yes (prod)
STRIPE_PRICE_ID Stripe price ID for subscription - Yes (prod)

Health Checks

Both the API and frontend images expose a /health endpoint but do not include a built-in HEALTHCHECK instruction. It is the deployer's responsibility to configure health checks in their compose file or orchestrator.

Service Endpoint Port Healthy response
api /health 8080 200 OK
frontend /health 8080 200 OK "healthy"

Docker Compose example

services:
  api:
    image: your-registry/saas-api:latest
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
      interval: 30s
      timeout: 3s
      start_period: 5s
      retries: 3

  frontend:
    image: your-registry/saas-frontend:latest
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
      interval: 30s
      timeout: 3s
      start_period: 5s
      retries: 3

Architecture Decisions

Why Actix-Web?

  • Excellent performance and async support
  • Strong ecosystem for web services
  • Type-safe request handling
  • Battle-tested in production

Why SQLx over other ORMs?

  • Compile-time query verification
  • No runtime ORM overhead
  • Direct SQL with type safety
  • Async-first design

JWT Strategy

  • Algorithm: EdDSA (Ed25519) - faster and more secure than RS256
  • Access Token: 15 minutes - short-lived for security
  • Refresh Token: 30 days - stored in database for revocation
  • Cookie Domain: .example.com - enables SSO across subdomains

Subdomain Routing

Traefik handles routing based on subdomain:

  • example.com -> Marketing site
  • app.example.com -> User dashboard
  • api.example.com -> Backend API
  • admin.example.com -> Admin panel
  • *.example.com -> Individual applications

Will this work on any machine?

Almost — the only manual step is each developer needs to add the /etc/hosts entries:

  • 127.0.0.1 a8n.test
  • 127.0.0.1 app.a8n.test
  • 127.0.0.1 api.a8n.test
  • 127.0.0.1 rus.a8n.test

Everything else (Traefik routing, cookie domain, CORS) is baked into the compose files and code. So for any new dev machine it's: clone, add hosts entries, docker compose up.

You could automate the hosts step with a Makefile target or a setup script if you wanted to reduce friction for the other two devs.

The Firefox proxy method

That approach involves configuring Firefox (or a PAC file) to route *.a8n.test traffic through a local proxy. It avoids touching /etc/hosts but adds complexity. With your current setup — /etc/hosts + Traefik — you get the same result more simply. No need for it.

What changes for production?

Almost nothing — your production docker-compose.yml is already set up correctly:

Concern Dev (current) Production (already handled)
DNS /etc/hosts → 127.0.0.1 Real DNS records for *.example.com
TLS None (HTTP) Let's Encrypt via Traefik (already configured)
Cookie domain .a8n.test (explicit env var) .example.com (auto-set when ENVIRONMENT=production)
Cookie Secure flag false true (from config.is_production())
CORS .a8n.test + .example.com .example.com (already in code)
Vite allowedHosts Needed for dev server N/A — production serves static files via nginx

The only thing to confirm is that each child app in production shares the same JWT_SECRET env var as the main API. Your production compose already has JWT_SECRET: ${JWT_SECRET} on the API — just make sure RUS and any other child apps get the same value.

License

Proprietary - All Rights Reserved

About

SaaS application

Resources

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •