A SaaS platform hosting developer and productivity tools. We sell convenience, reliability, and managed hosting for open-source applications.
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
| Application | Description | Subdomain |
|---|---|---|
| RUS | URL shortening with QR codes | rus.example.com |
| Rusty Links | Bookmark management | rustylinks.example.com |
| 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 |
| Stalwart (self-hosted) | |
| Monitoring | Prometheus, Grafana |
| Error Tracking | GlitchTip (self-hosted) |
- Docker and Docker Compose
- Rust toolchain (for local development)
- Bun
- Git
-
Clone the repository:
git clone https://github.com/your-org/a8n-tools.git cd a8n-tools -
Copy environment file:
cp .env.example .env
-
Start the development environment:
just dev
-
Access the applications:
- Frontend: http://localhost:5173
- API: http://localhost:18080
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
├── compose.dev.yml # Development environment
├── Justfile # Development commands
└── .env.example # Environment template
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
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:coverageRun 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;"
To promote a user to admin, connect to the database and update their role:
just db-shellUPDATE 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.
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- Create a handler in
api/src/handlers/ - Define the route in
api/src/routes/ - Register the route in
api/src/routes/mod.rs
Example:
// api/src/handlers/example.rs
use actix_web::{web, HttpRequest, HttpResponse};
use crate::errors::AppError;
use crate::responses::{get_request_id, success};
pub async fn get_item(
req: HttpRequest,
) -> Result<HttpResponse, AppError> {
let request_id = get_request_id(&req);
Ok(success(serde_json::json!({ "item": "value" }), request_id))
}- Create the page component in
frontend/src/pages/ - Add the route in
frontend/src/App.tsx - Update navigation if needed
| 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 |
ENVIRONMENT |
production or development |
production |
No |
APP_NAME |
App name used in email subjects and templates | localhost |
No |
APP_URL |
Frontend base URL for email links | Falls back to CORS_ORIGIN, then http://localhost:5173 |
No |
CORS_ORIGIN |
Allowed CORS origin (frontend URL) | http://localhost:5173 |
No |
COOKIE_DOMAIN |
Cookie domain for cross-subdomain auth (e.g. .example.com) |
None (exact hostname) | Yes (prod) |
JWT_SECRET |
Shared JWT signing secret | - | Yes (prod) |
TOTP_ENCRYPTION_KEY |
Hex-encoded 32-byte key for encrypting TOTP secrets | Zero bytes (dev only) | 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) |
SMTP_HOST |
SMTP server hostname | localhost |
No |
SMTP_PORT |
SMTP server port | 465 |
No |
SMTP_FROM |
Sender email (format: Name <email> or email) |
noreply@localhost |
No |
SMTP_USERNAME |
SMTP auth username | - | No |
SMTP_PASSWORD |
SMTP auth password | - | No |
EMAIL_ENABLED |
Force enable email sending in dev | false |
No |
| Variable | Description | Default | Required |
|---|---|---|---|
VITE_API_URL |
API base URL | http://localhost:18080 |
Yes (prod) |
VITE_APP_DOMAIN |
Application domain | localhost |
No |
VITE_SHOW_BUSINESS_PRICING |
Show business pricing tier | false |
No |
Frontend env vars are injected at runtime via a Caddy template endpoint (obfuscated path), not baked into the build. This allows deploying the same image to different environments by changing container env vars.
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" |
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- Excellent performance and async support
- Strong ecosystem for web services
- Type-safe request handling
- Battle-tested in production
- Compile-time query verification
- No runtime ORM overhead
- Direct SQL with type safety
- Async-first design
- 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
Traefik handles routing based on subdomain:
example.com-> Marketing siteapp.example.com-> User dashboardapi.example.com-> Backend APIadmin.example.com-> Admin panel*.example.com-> Individual applications
| Concern | Dev | Production |
|---|---|---|
| DNS | localhost |
Real DNS records for *.example.com |
| TLS | None (HTTP) | Let's Encrypt via Traefik |
| Cookie domain | None (exact hostname) | .example.com (set via COOKIE_DOMAIN) |
| Cookie Secure flag | false |
true (when ENVIRONMENT=production) |
| CORS | http://localhost:5173 |
Frontend URL (set via CORS_ORIGIN) |
| Frontend serving | Vite dev server | Static files via Caddy |
| Email links | http://localhost:5173 |
Derived from APP_URL or CORS_ORIGIN |
Each child app (RUS, Rusty Links) must share the same JWT_SECRET as the main API for SSO to work.
Proprietary - All Rights Reserved