feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1
@@ -7,10 +7,11 @@ on:
|
||||
|
||||
env:
|
||||
REGISTRY: git.gabrielkaszewski.dev
|
||||
IMAGE: git.gabrielkaszewski.dev/gkaszewski/thoughts
|
||||
BACKEND_IMAGE: git.gabrielkaszewski.dev/gkaszewski/thoughts
|
||||
FRONTEND_IMAGE: git.gabrielkaszewski.dev/gkaszewski/thoughts-frontend
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -26,25 +27,61 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGE }}
|
||||
images: ${{ env.BACKEND_IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and push backend
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max
|
||||
cache-from: type=registry,ref=${{ env.BACKEND_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.BACKEND_IMAGE }}:buildcache,mode=max
|
||||
|
||||
build-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FRONTEND_IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push frontend
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./thoughts-frontend
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}
|
||||
NEXT_PUBLIC_SERVER_SIDE_API_URL=${{ secrets.NEXT_PUBLIC_SERVER_SIDE_API_URL }}
|
||||
cache-from: type=registry,ref=${{ env.FRONTEND_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.FRONTEND_IMAGE }}:buildcache,mode=max
|
||||
|
||||
deploy:
|
||||
needs: build-and-push
|
||||
needs: [build-backend, build-frontend]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
steps:
|
||||
@@ -55,5 +92,6 @@ jobs:
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
script: |
|
||||
docker pull ${{ env.IMAGE }}:latest
|
||||
docker pull ${{ env.BACKEND_IMAGE }}:latest
|
||||
docker pull ${{ env.FRONTEND_IMAGE }}:latest
|
||||
docker compose -f /opt/thoughts/docker-compose.yml up -d
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
database:
|
||||
image: postgres:15-alpine
|
||||
image: postgres:16-alpine
|
||||
container_name: thoughts-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -17,19 +17,21 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
|
||||
backend:
|
||||
container_name: thoughts-backend
|
||||
image: thoughts-backend:latest
|
||||
api:
|
||||
container_name: thoughts-api
|
||||
image: registry.gabrielkaszewski.dev/thoughts:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
- RUST_BACKTRACE=1
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database/${POSTGRES_DB}
|
||||
- HOST=0.0.0.0
|
||||
- PORT=8000
|
||||
- PREFORK=1
|
||||
- AUTH_SECRET=${AUTH_SECRET}
|
||||
- BASE_URL=https://thoughts.gabrielkaszewski.dev
|
||||
RUST_LOG: info
|
||||
RUST_ENV: production
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database/${POSTGRES_DB}
|
||||
HOST: 0.0.0.0
|
||||
PORT: 8000
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
BASE_URL: ${BASE_URL}
|
||||
NATS_URL: ${NATS_URL}
|
||||
CORS_ORIGINS: ${CORS_ORIGINS:-*}
|
||||
ALLOW_REGISTRATION: ${ALLOW_REGISTRATION:-false}
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_healthy
|
||||
@@ -40,22 +42,41 @@ services:
|
||||
retries: 5
|
||||
networks:
|
||||
- internal
|
||||
- nats
|
||||
|
||||
worker:
|
||||
container_name: thoughts-worker
|
||||
image: registry.gabrielkaszewski.dev/thoughts:latest
|
||||
entrypoint: ["./thoughts-worker"]
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RUST_LOG: info
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database/${POSTGRES_DB}
|
||||
BASE_URL: ${BASE_URL}
|
||||
NATS_URL: ${NATS_URL}
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- internal
|
||||
- nats
|
||||
|
||||
frontend:
|
||||
container_name: thoughts-frontend
|
||||
image: thoughts-frontend:latest
|
||||
image: registry.gabrielkaszewski.dev/thoughts-frontend:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NEXT_PUBLIC_SERVER_SIDE_API_URL: http://api:8000
|
||||
PORT: 3000
|
||||
HOSTNAME: 0.0.0.0
|
||||
depends_on:
|
||||
- backend
|
||||
api:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
environment:
|
||||
- NEXT_PUBLIC_SERVER_SIDE_API_URL=http://proxy/api
|
||||
- PORT=3000
|
||||
- HOSTNAME=0.0.0.0
|
||||
networks:
|
||||
- internal
|
||||
|
||||
@@ -66,7 +87,7 @@ services:
|
||||
depends_on:
|
||||
frontend:
|
||||
condition: service_healthy
|
||||
backend:
|
||||
api:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- internal
|
||||
@@ -83,7 +104,13 @@ services:
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
# Shared NATS network — must already exist on the host (external: true).
|
||||
# Set NATS_NETWORK env var to match your shared network name (default: nats).
|
||||
nats:
|
||||
name: ${NATS_NETWORK:-nats}
|
||||
external: true
|
||||
traefik:
|
||||
name: traefik
|
||||
external: true
|
||||
|
||||
@@ -1,129 +1,17 @@
|
||||
# clean-axum
|
||||
# ⚠️ DEPRECATED — thoughts-backend (v1)
|
||||
|
||||
Axum scaffold with clean architecture.
|
||||
> **This directory is the original v1 implementation and is no longer maintained.**
|
||||
> It will be removed in a future release.
|
||||
|
||||
You probably don't need [Rust on Rails](https://github.com/loco-rs/loco).
|
||||
## Use v2 instead
|
||||
|
||||
Refer to [this post](https://kigawas.me/posts/rustacean-clean-architecture-approach/) for rationale and background.
|
||||
The active codebase lives at the **repository root** (`/crates/`). It is a complete rewrite with:
|
||||
|
||||
## Features
|
||||
- Hexagonal (Ports & Adapters) architecture
|
||||
- Full ActivityPub federation
|
||||
- Remote actor discovery and profile browsing
|
||||
- NATS JetStream event bus
|
||||
- Clean REST API with content negotiation
|
||||
- Next.js frontend (`/thoughts-frontend/`)
|
||||
|
||||
- [Axum](https://github.com/tokio-rs/axum) framework
|
||||
- [SeaORM](https://github.com/SeaQL/sea-orm) domain models
|
||||
- Completely separated API routers and DB-related logic (named "persistence" layer)
|
||||
- Completely separated input parameters, queries and output schemas
|
||||
- OpenAPI documentation ([Swagger UI](https://clean-axum.shuttleapp.rs/docs) and [Scalar](https://clean-axum.shuttleapp.rs/scalar)) powered by [Utoipa](https://github.com/juhaku/utoipa)
|
||||
- Error handling with [Anyhow](https://github.com/dtolnay/anyhow)
|
||||
- Custom parameter validation with [validator](https://github.com/Keats/validator)
|
||||
- Optional [Shuttle](https://www.shuttle.rs/) runtime
|
||||
- Optional [prefork](https://docs.rs/prefork/latest/prefork/) workers for maximizing performance on Linux
|
||||
|
||||
## Module hierarchy
|
||||
|
||||
### API logic
|
||||
|
||||
- `api::routers`: Axum endpoints
|
||||
- `api::error`: Models and traits for error handling
|
||||
- `api::extractor` Custom Axum extractors
|
||||
- `api::extractor::json`: `Json` for bodies and responses
|
||||
- `api::extractor::valid`: `Valid` for JSON body validation
|
||||
- `api::validation`: JSON validation model based on `validator`
|
||||
- `api::models`: Non domain model API models
|
||||
- `api::models::response`: JSON error response
|
||||
|
||||
### OpenAPI documentation
|
||||
|
||||
- `doc`: Utoipa doc declaration
|
||||
|
||||
### API-agonistic application logic
|
||||
|
||||
Main concept: Web framework is replaceable.
|
||||
|
||||
All modules here should not include any specific API web framework logic.
|
||||
|
||||
- `app::persistence`: DB manipulation (CRUD) functions
|
||||
- `app::config`: DB or API server configuration
|
||||
- `app::state`: APP state, e.g. DB connection
|
||||
- `app::error`: APP errors used by `api::error`. e.g. "User not found"
|
||||
|
||||
### DB/API-agnostic domain models
|
||||
|
||||
Main concept: Database (Sqlite/MySQL/PostgreSQL) is replaceable.
|
||||
|
||||
Except `models::domains` and `migration`, all modules are ORM library agnostic.
|
||||
|
||||
- `models::domains`: SeaORM domain models
|
||||
- `models::params`: Serde input parameters for creating/updating domain models in DB
|
||||
- `models::schemas`: Serde output schemas for combining different domain models
|
||||
- `models::queries`: Serde queries for filtering domain models
|
||||
- `migration`: SeaORM migration files
|
||||
|
||||
### Unit and integration tests
|
||||
|
||||
- `tests::api`: API integration tests. Hierarchy is the same as `api::routers`
|
||||
- `tests::app::persistence`: DB/ORM-related unit tests. Hierarchy is the same as `app::persistence`
|
||||
|
||||
### Others
|
||||
|
||||
- `utils`: Utility functions
|
||||
- `main`: Tokio and Shuttle conditional entry point
|
||||
|
||||
## Run
|
||||
|
||||
### Start server
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# touch dev.db
|
||||
# cargo install sea-orm-cli
|
||||
# sea-orm-cli migrate up
|
||||
cargo run
|
||||
|
||||
# or for production
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
### Call API
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"username":"aaa"}'
|
||||
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"username":"abc"}'
|
||||
curl http://localhost:3000/users\?username\=a
|
||||
```
|
||||
|
||||
### OpenAPI doc (Swagger UI/Scalar)
|
||||
|
||||
```bash
|
||||
open http://localhost:3000/docs
|
||||
open http://localhost:3000/scalar
|
||||
```
|
||||
|
||||
## Start Shuttle local server
|
||||
|
||||
```bash
|
||||
# cargo install cargo-shuttle
|
||||
cargo shuttle run
|
||||
```
|
||||
|
||||
Make sure docker engine is running, otherwise:
|
||||
|
||||
```bash
|
||||
brew install colima docker
|
||||
colima start
|
||||
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
|
||||
```
|
||||
|
||||
## Shuttle deployment
|
||||
|
||||
```bash
|
||||
cargo shuttle login
|
||||
cargo shuttle deploy
|
||||
```
|
||||
|
||||
## Benchmark
|
||||
|
||||
```bash
|
||||
# edit .env to use Postgres
|
||||
cargo run --release
|
||||
wrk --latency -t20 -c50 -d10s http://localhost:3000/users\?username\=
|
||||
```
|
||||
Do not build, run, or modify anything in this directory.
|
||||
|
||||
Reference in New Issue
Block a user