architecture
System Architecture Documentation
Generated: 2025-11-15
Project: trip2g
Type: Full-Stack Web Application (Monolith)
Architecture Overview
trip2g is a publishing platform built as a monolithic full-stack application that transforms Obsidian markdown vaults into websites with subscription-based access control and Telegram channel integration.
High-Level Architecture
┌─────────────────────────────────────────────────────────────┐
│ Client Layer │
├──────────────────────┬──────────────────────────────────────┤
│ Web Browser │ Obsidian Plugin │
│ ($mol Components) │ (API Key Auth) │
└──────────────────────┴──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ HTTP/GraphQL Layer │
│ ┌───────────┐ ┌────────────┐ ┌────────────────────────┐ │
│ │ fasthttp │ │ GraphQL │ │ Webhooks │ │
│ │ Router │─▶│ API │ │ - NowPayments │ │
│ │ │ │ (gqlgen) │ │ - Patreon │ │
│ └───────────┘ └────────────┘ │ - Telegram │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Business Logic Layer │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Use Cases (internal/case/*) │ │
│ │ - GraphQL Resolvers (admin & public mutations) │ │
│ │ - HTTP Endpoints (webhooks, git protocol) │ │
│ │ - Background Jobs (payment processing, sync) │ │
│ │ - Cron Jobs (cleanup, member removal) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ Data Access Layer │ │ Integration Layer │
│ ┌────────────────────┐ │ │ ┌───────────────────┐ │
│ │ SQLite (WAL mode) │ │ │ │ Telegram Bots │ │
│ │ - sqlc queries │ │ │ │ - Bot API │ │
│ │ - Migrations │ │ │ │ - Publishing │ │
│ └────────────────────┘ │ │ └───────────────────┘ │
│ ┌────────────────────┐ │ │ ┌───────────────────┐ │
│ │ Job Queues │ │ │ │ Payment Providers│ │
│ │ - goqite │ │ │ │ - NowPayments │ │
│ │ - backlite │ │ │ │ - Patreon │ │
│ └────────────────────┘ │ │ │ - Boosty │ │
│ │ │ └───────────────────┘ │
│ │ │ ┌───────────────────┐ │
│ │ │ │ Object Storage │ │
│ │ │ │ - MinIO (S3) │ │
│ │ │ └───────────────────┘ │
└──────────────────────────┘ └──────────────────────────┘
Core Components
1. HTTP/GraphQL Layer
Technology: fasthttp + gqlgen
Responsibilities:
- HTTP request routing
- GraphQL query/mutation handling
- WebSocket support (if used)
- Authentication middleware
- Request context management
Key Files:
cmd/server/main.go- Server initializationinternal/router/router.go- Route registrationinternal/graph/schema.graphqls- GraphQL schemainternal/graph/schema.resolvers.go- Resolver implementations
Authentication Flow:
User Request → Router
↓
Extract Auth Header (Bearer token or X-Api-Key)
↓
Validate Token (hotauthtoken package)
↓
Set User Context
↓
Pass to Resolver/Handler
2. Business Logic Layer (Use Cases)
Pattern: Single-responsibility use cases
Structure:
internal/case/
├── {usecasename}/
│ ├── resolve.go # Business logic
│ ├── resolve_test.go # Unit tests
│ └── endpoint.go # HTTP endpoint (if needed)
Interface Pattern:
type Env interface {
// Only methods THIS use case needs
GetUser(ctx context.Context, id int64) (*db.User, error)
UpdateUser(ctx context.Context, params db.UpdateUserParams) error
}
func Resolve(ctx context.Context, env Env, input Input) (Payload, error) {
// 1. Validate input
// 2. Check permissions
// 3. Execute business logic
// 4. Return result or error
}
Benefits:
- Testable (mock Env interface)
- Isolated (no global state)
- Explicit dependencies
- Single responsibility
3. Data Access Layer
Technology: SQLite + sqlc
Why SQLite:
- Embedded (no separate database server)
- ACID compliance
- WAL mode for concurrent reads/writes
- Sufficient for current scale
- Simple deployment
sqlc Pattern:
-- internal/db/queries.sql
-- name: GetUser :one
select * from users where id = ?;
-- name: CreateUser :one
insert into users (email, created_at)
values (?, datetime('now'))
returning *;
Generated Code:
// internal/db/queries.sql.go (auto-generated)
func (q *Queries) GetUser(ctx context.Context, id int64) (User, error)
func (q *Queries) CreateUser(ctx context.Context, email string) (User, error)
Benefits:
- Type-safe SQL
- Compile-time query validation
- No ORM overhead
- Direct SQL control
4. Content Processing Pipeline
Markdown → HTML Flow:
Obsidian Vault (.md files)
│
▼
API Key Upload (pushNotes mutation)
│
▼
Parse Markdown (goldmark)
│
┌────┴────┐
▼ ▼
Frontmatter Content
(meta tags) (with wikilinks)
│
▼
Process Wikilinks
- Convert [[Note]] → /note
- Resolve paths
- Track backlinks
│
▼
Process Enclaves
- Extract protected sections
- Map to subgraphs
│
▼
Generate HTML
- Goldmark rendering
- Syntax highlighting
- TOC generation
│
▼
Store NoteVersion
- content (markdown)
- html (rendered)
- metadata
│
▼
Extract Assets
- Images
- Attachments
- Upload to MinIO
│
▼
Ready for Publishing
Key Components:
internal/mdloader/- Markdown parsinginternal/noteloader/- Version managementinternal/layoutloader/- Hugo layout integrationgoldmark+ extensions - Markdown → HTML
5. Access Control System
Subgraph-Based Access:
User
├─ user_subgraph_accesses
│ ├─ subgraph_id
│ ├─ expires_at
│ └─ purchase_id / created_by
│
└─ Can Access Notes with matching subgraph
Note Version
└─ Belongs to Subgraphs (via frontmatter)
└─ free: boolean (override)
Access Check Flow:
1. User requests note
2. Check if note is free → Allow
3. Check user's active subgraph accesses
4. Match note's subgraphs with user's access
5. Allow if ANY match, deny otherwise
Grant Sources:
- Direct purchase (via offer)
- Manual admin grant
- Patreon tier sync
- Boosty tier sync
- Telegram chat membership
6. Payment Integration Architecture
Multi-Provider Strategy:
┌─────────────┐
│ Offer │
│ $X for │
│ Subgraphs │
└──────┬──────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Crypto │ │ Patreon │ │ Boosty │
│NowPay't │ │ Tiers │ │ Tiers │
└────┬────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
Purchase Member Sync Member Sync
Webhook Webhook/Cron Cron Job
│ │ │
└──────────────┼────────────────┘
▼
Create user_subgraph_access
(with expiration)
Providers:
-
NowPayments (Crypto)
- IPN webhook
- Purchase record created
- Access granted immediately
-
Patreon
- OAuth2 credentials
- Webhook for real-time updates
- Cron job for periodic sync
- Tier → Subgraph mapping
-
Boosty
- Cookie-based API
- Cron job for sync (no webhooks)
- Tier → Subgraph mapping
Sync Process:
1. Fetch members from provider API
2. Match email to platform users
3. Determine tier → subgraph access
4. Create/update user_subgraph_access
5. Set expiration based on tier status
6. Mark old accesses as revoked
7. Telegram Integration Architecture
Multi-Bot Support:
┌──────────────────────────────────────────────┐
│ Telegram Bot Framework │
├───────────┬─────────────┬────────────────────┤
│ Bot 1 │ Bot 2 │ Bot N │
│ (Chat A) │ (Chat B) │ (Chat C) │
└─────┬─────┴──────┬──────┴──────┬─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────┐
│ Update Handler (handletgupdate) │
│ ┌────────────┐ ┌───────────────────────┐ │
│ │ Commands │ │ Events │ │
│ │ /start │ │ new_chat_members │ │
│ │ /attach │ │ left_chat_member │ │
│ │ /help │ │ message │ │
│ └────────────┘ └───────────────────────┘ │
└─────────────────────────────────────────────┘
Publishing Flow:
Admin creates telegram_publish_note
│
├─ publish_at (scheduled time)
├─ tags (categories)
└─ note_path_id
│
▼
Cron job checks for ready notes
│
▼
Convert note to Telegram format
- MarkdownV2 conversion
- Wikilink → URL
- Image handling
│
▼
Send to all chats with matching tags
- Scheduled tags → scheduled posts
- Instant tags → immediate posts
│
▼
Track sent messages
- telegram_publish_sent_messages
- content_hash for updates
│
▼
Mark as published or retry on error
Chat Access Control:
User clicks "Join Chat" on website
│
▼
Create tg_bot_chat_subgraph_access
(joined_at = null)
│
▼
Generate invite link
│
▼
User joins Telegram chat
│
▼
Bot receives new_chat_members event
│
▼
Update joined_at timestamp
│
▼
Cron job monitors for expiration
│
▼
Remove user from chat when access expires
8. Background Job Architecture
Two Queue Systems:
1. goqite (Priority Queue)
- SQLite-based
- Priority support
- Retry logic
- Used for: Payment processing, email sending
2. backlite (Task Queue)
- SQLite-based
- Task scheduling
- Completion tracking
- Used for: Heavy processing, cleanup
Job Pattern:
type Job struct {
Params JobParams
}
func (j *Job) Handle(ctx context.Context, env Env) error {
// Execute job
return nil
}
// Enqueue
jobs.Enqueue(ctx, &MyJob{Params: ...})
9. Cron Job System
Architecture:
┌──────────────────────────────────────┐
│ Cron Manager (robfig/cron) │
├──────────────────────────────────────┤
│ Job Registry │
│ ├─ remove_expired_tg_chat_members │
│ │ Schedule: 0 0 * * * * (hourly) │
│ ├─ clear_cronjob_execution_history │
│ │ Schedule: 0 0 0 * * * (daily) │
│ └─ sync_patreon_members │
│ Schedule: 0 */30 * * * * (30m) │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ cron_jobs (database table) │
│ - name, enabled, expression │
│ - Can be toggled via admin panel │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ cron_job_executions (history) │
│ - job_id, started_at, finished_at │
│ - status, error_message, report │
└──────────────────────────────────────┘
Job Lifecycle:
- Server starts → Register jobs
- Cron manager schedules jobs
- Job executes → Create execution record
- Update status (pending → running → completed/failed)
- Store result or error
Data Flow Patterns
1. Content Publishing Flow
Obsidian → API Key Upload → Version Storage → Release → Public Access
Steps:
- Editor creates/updates markdown in Obsidian
- Plugin detects changes
pushNotesmutation uploads content- Server creates new note_version
- Extracts assets, uploads to MinIO
- Admin creates release
- Selected versions become live
- Users can access published content
2. Subscription Access Flow
User Purchase → Access Grant → Content Unlock
Steps:
- User selects offer
- Payment provider processes payment
- Webhook/sync creates purchase record
- System creates user_subgraph_access
- User can now access matching subgraph content
- Access expires based on purchase lifetime
3. Telegram Publishing Flow
Schedule Post → Cron Check → Convert → Send → Track
Steps:
- Admin schedules note for publishing
- Assigns tags (determines target chats)
- Cron job checks for ready notes
- Converts markdown to Telegram format
- Sends to all chats with matching tags
- Records sent messages for update tracking
Security Architecture
Authentication
JWT Tokens (User Auth):
- Email-based sign-in codes
- Short-lived tokens
- Stored in hotauthtoken
API Keys (Programmatic Access):
- Long-lived keys for Obsidian plugin
- Logged in api_key_logs
- Can be disabled by admin
Admin Authorization:
- Checked at start of every admin mutation
CurrentAdminUserToken(ctx)pattern
Authorization
Layered Checks:
- Authentication - Who are you?
- Role Check - Are you admin/user/guest?
- Resource Check - Can you access this note/subgraph?
- Action Check - Can you perform this operation?
Example Flow:
// Admin mutation
token, err := env.CurrentAdminUserToken(ctx)
if err != nil {
return nil, err // Not authenticated as admin
}
// Resource check
canAccess := checkUserSubgraphAccess(userID, subgraphID)
if !canAccess {
return ErrorPayload{Message: "Access denied"}
}
Input Validation
Framework: ozzo-validation
Pattern:
func validateRequest(r *Input) *model.ErrorPayload {
return model.NewOzzoError(ozzo.ValidateStruct(r,
ozzo.Field(&r.Email, validation.Required, is.Email),
ozzo.Field(&r.Amount, validation.Min(0)),
))
}
Validation happens:
- At GraphQL input layer
- In business logic (Resolve functions)
- Before database operations
Scalability Considerations
Current Architecture (Monolith)
Strengths:
- Simple deployment
- Low latency (no network calls between services)
- Easy to reason about
- Suitable for current scale
Bottlenecks:
- Single SQLite database
- In-process job queues
- No horizontal scaling
Future Migration Path
If scale demands:
-
Database: SQLite → PostgreSQL
- sqlc supports PostgreSQL
- Minimal code changes needed
-
Job Queues: goqite → Redis/RabbitMQ
- Replace in-process queues
- Separate worker processes
-
File Storage: Already using MinIO (S3-compatible)
- Ready for cloud object storage
-
Caching: Add Redis for:
- Session storage
- Frequently accessed notes
- Rendered HTML cache
-
Microservices (if needed):
- Content Service (markdown processing)
- Auth Service (user management)
- Payment Service (payment processing)
- Telegram Service (bot handling)
Performance Optimizations
Currently Implemented:
- SQLite WAL mode (concurrent reads)
- Connection pooling
- Background job processing
- Index optimization
Potential Additions:
- CDN for static assets
- Edge caching for public content
- Database query caching
- GraphQL query complexity limits
- Rate limiting on expensive operations
Deployment Architecture
Single Server:
┌─────────────────────────────────────┐
│ trip2g Binary │
│ ┌──────────────────────────────┐ │
│ │ HTTP Server (fasthttp) │ │
│ ├──────────────────────────────┤ │
│ │ GraphQL API │ │
│ ├──────────────────────────────┤ │
│ │ Background Workers │ │
│ ├──────────────────────────────┤ │
│ │ Cron Jobs │ │
│ └──────────────────────────────┘ │
├─────────────────────────────────────┤
│ SQLite Database │
│ (data.sqlite3) │
└─────────────────────────────────────┘
With Reverse Proxy:
Internet → Caddy/Nginx → trip2g Binary
↓
SQLite DB
MinIO Storage
Key Design Patterns
- Repository Pattern - sqlc-generated queries
- Use Case Pattern - Single-responsibility business logic
- Dependency Injection - Env interface pattern
- CQRS Light - Read vs. write separation in queries
- Event Sourcing (Partial) - Note versioning, audit logs
- Webhook Pattern - Payment/Patreon integrations
- Queue Pattern - Background job processing
- Strategy Pattern - Multiple payment providers
Technology Decisions
Why Go?
- Performance
- Strong typing
- Excellent stdlib
- Good concurrency support
- Fast compilation
- Single binary deployment
Why SQLite?
- Zero configuration
- ACID compliance
- Fast for read-heavy workloads
- Embedded (no separate server)
- Simple backups (copy file)
Why GraphQL?
- Flexible queries
- Type safety
- Self-documenting API
- Efficient data fetching
- Good tooling ecosystem
Why $mol Framework?
- Reactive by design
- No virtual DOM overhead
- Component-based
- TypeScript support
- Small bundle size
Why Monolith?
- Faster development
- Simpler deployment
- Sufficient for current scale
- Easy debugging
- Low operational complexity