21 KiB
Development & Architecture Comprehensive Guide
Overview
This guide consolidates all development, architecture, and design system documentation for the Sojorn platform, covering the philosophical foundations, technical architecture, and implementation patterns.
Architecture Philosophy
Core Principles
Sojorn's friendliness is not aspirational—it is structural. The architecture enforces behavioral philosophy through database constraints, API design, and systematic patterns that make certain behaviors impossible, not just discouraged.
1. Blocking: Complete Disappearance
Principle: When you block someone, they disappear from your world and you from theirs.
Implementation:
- Database function:
has_block_between(user_a, user_b)checks bidirectional blocks - API middleware prevents blocked users from:
- Seeing each other's profiles
- Seeing each other's posts
- Seeing each other's follows
- Interacting in any way
Effect: No notifications, no traces, no conflict. The system enforces separation silently.
2. Consent: Conversation Requires Mutual Follow
Principle: You cannot reply to someone unless you mutually follow each other.
Implementation:
- Database function:
is_mutual_follow(user_a, user_b)verifies bidirectional following - Comment creation requires mutual follow relationship
- API endpoints enforce conversation gating
Effect: Unwanted replies are impossible. Conversation is opt-in by structure.
3. Exposure: Opt-In by Default
Principle: Users choose what content they see. Filtering is private and encouraged.
Implementation:
- All categories except
generalhavedefault_off = true - Users must explicitly enable categories to see posts
- Feed algorithms respect user preferences
Effect: Users control their content exposure without social pressure.
Technical Architecture
System Overview
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Flutter App │ │ Go Backend │ │ PostgreSQL │
│ │ │ │ │ │
│ - UI/UX │◄──►│ - REST API │◄──►│ - Data Store │
│ - State Mgmt │ │ - Business Logic│ │ - Constraints │
│ - Navigation │ │ - Validation │ │ - Functions │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Local Storage │ │ File System │ │ Extensions │
│ │ │ │ │ │
│ - Secure Storage│ │ - Uploads │ │ - PostGIS │
│ - Cache │ │ - Logs │ │ - pg_trgm │
│ - Preferences │ │ - Temp Files │ │ - uuid-ossp │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Backend Architecture
Layer Structure
┌─────────────────────────────────────────┐
│ API Layer │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ Gin │ │ Middleware │ │
│ │ Router │ │ - Auth │ │
│ │ │ │ - CORS │ │
│ │ │ │ - Rate Limit │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Service Layer │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ Business │ │ External │ │
│ │ Logic │ │ Services │ │
│ │ │ │ - FCM │ │
│ │ │ │ - R2 Storage │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Repository Layer │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ Data │ │ Database │ │
│ │ Access │ │ - PostgreSQL │ │
│ │ │ │ - Migrations │ │
│ │ │ │ - Queries │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────┘
Key Components
1. Authentication & Authorization
- JWT-based authentication with refresh tokens
- Role-based access control
- Session management with secure cookies
2. Data Validation
- Structured request/response models
- Input sanitization and validation
- Error handling with proper HTTP status codes
3. Business Logic Services
- User management and relationships
- Content moderation and filtering
- Notification and messaging systems
4. External Integrations
- Firebase Cloud Messaging
- Cloudflare R2 storage
- Email services
Database Architecture
Core Schema Design
Identity & Relationships
profiles (users, identity, settings)
├── follows (mutual relationships)
├── blocks (complete separation)
└── user_category_settings (content preferences)
Content & Engagement
posts (content, metadata)
├── post_metrics (engagement data)
├── post_likes (boosts only)
├── post_saves (private bookmarks)
└── comments (mutual-follow-only)
Moderation & Trust
reports (community moderation)
├── trust_state (harmony scoring)
└── audit_log (transparency trail)
Database Functions
Relationship Checking
-- Bidirectional blocking
CREATE OR REPLACE FUNCTION has_block_between(user_a UUID, user_b UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM blocks
WHERE (blocker_id = user_a AND blocked_id = user_b)
OR (blocker_id = user_b AND blocked_id = user_a)
);
$$ LANGUAGE SQL;
-- Mutual follow verification
CREATE OR REPLACE FUNCTION is_mutual_follow(user_a UUID, user_b UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM follows f1
JOIN follows f2 ON f1.follower_id = f2.following_id
AND f1.following_id = f2.follower_id
WHERE f1.follower_id = user_a AND f1.following_id = user_b
);
$$ LANGUAGE SQL;
Rate Limiting
-- Posting rate limits
CREATE OR REPLACE FUNCTION can_post(user_id UUID)
RETURNS BOOLEAN AS $$
SELECT get_post_rate_limit(user_id) > 0;
$$ LANGUAGE SQL;
Design System
Visual Philosophy
Warm, Not Overwhelming
- Warm neutrals (beige/paper tones) instead of cold grays
- Soft shadows, never harsh
- Muted semantic colors that inform without alarming
Modern, Not Trendy
- Timeless color palette
- Classic typography hierarchy
- Subtle animations and transitions
Text-Forward
- Generous line height (1.6-1.65 for body text)
- Optimized for reading, not scanning
- Clear hierarchy without relying on color
Intentionally Slow
- Animation durations: 300-400ms
- Ease curves that feel deliberate
- No jarring transitions
Color System
Background Palette
background = #F8F7F4 // Warm off-white (like paper)
surface = #FFFFFD // Barely warm white
surfaceElevated = #FFFFFF // Pure white for cards
surfaceVariant = #F0EFEB // Subtle warm gray (inputs)
Semantic Colors
primary = #6B5B95 // Soft purple (friendly authority)
secondary = #8B7355 // Warm brown (earth tone)
success = #6B8E6F // Muted green (gentle confirmation)
warning = #B8956A // Soft amber (warm caution)
error = #B86B6B // Muted rose (gentle error)
Border System
borderSubtle = #E8E6E1 // Barely visible dividers
border = #D8D6D1 // Default borders
borderStrong = #C8C6C1 // Emphasized borders
Typography
Font Hierarchy
// Display
displayLarge = 32px / 48px / 300
displayMedium = 28px / 42px / 300
displaySmall = 24px / 36px / 300
// Headings
headlineLarge = 20px / 30px / 400
headlineMedium = 18px / 27px / 400
headlineSmall = 16px / 24px / 500
// Body
bodyLarge = 16px / 26px / 400
bodyMedium = 14px / 22px / 400
bodySmall = 12px / 20px / 400
// Labels
labelLarge = 14px / 20px / 500
labelMedium = 12px / 16px / 500
labelSmall = 10px / 14px / 500
Typography Principles
- Line Height: 1.6-1.65 for body text (optimized for reading)
- Font Weight: Conservative use (300-500, rarely 700)
- Letter Spacing: Subtle adjustments for readability
- Color: Primary use of gray scale, semantic colors sparingly
Component System
Buttons
// Primary Button
ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(primary),
foregroundColor: MaterialStateProperty.all<Color>(surface),
elevation: MaterialStateProperty.all<double>(2),
padding: MaterialStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(horizontal: 24, vertical: 16)
),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))
)
)
// Secondary Button
ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(surfaceVariant),
foregroundColor: MaterialStateProperty.all<Color>(primary),
elevation: MaterialStateProperty.all<double>(1),
// ... same padding and shape
)
Cards
Card(
elevation: 3,
shadowColor: Colors.black.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)
),
child: Padding(
padding: EdgeInsets.all(16),
child: // content
)
)
Input Fields
InputDecoration(
filled: true,
fillColor: surfaceVariant,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: border)
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: primary, width: 2)
),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12)
)
Development Patterns
Code Organization
Flutter Structure
lib/
├── main.dart # App entry point
├── config/ # Configuration
│ ├── api_config.dart
│ └── theme_config.dart
├── models/ # Data models
│ ├── user.dart
│ ├── post.dart
│ └── chat.dart
├── providers/ # State management
│ ├── auth_provider.dart
│ ├── feed_provider.dart
│ └── theme_provider.dart
├── services/ # Business logic
│ ├── api_service.dart
│ ├── auth_service.dart
│ └── notification_service.dart
├── screens/ # UI screens
│ ├── auth/
│ ├── home/
│ └── chat/
├── widgets/ # Reusable components
│ ├── post_card.dart
│ ├── user_avatar.dart
│ └── chat_bubble.dart
└── utils/ # Utilities
├── constants.dart
├── helpers.dart
└── validators.dart
Go Structure
cmd/
└── api/
└── main.go # Application entry
internal/
├── config/ # Configuration
│ └── config.go
├── models/ # Data models
│ ├── user.go
│ ├── post.go
│ └── chat.go
├── handlers/ # HTTP handlers
│ ├── auth_handler.go
│ ├── post_handler.go
│ └── chat_handler.go
├── services/ # Business logic
│ ├── auth_service.go
│ ├── post_service.go
│ └── notification_service.go
├── repository/ # Data access
│ ├── user_repository.go
│ ├── post_repository.go
│ └── chat_repository.go
├── middleware/ # HTTP middleware
│ ├── auth.go
│ ├── cors.go
│ └── ratelimit.go
└── database/ # Database
├── migrations/
└── queries.go
State Management Patterns
Flutter Provider Pattern
// Authentication Provider
final authServiceProvider = Provider<AuthService>((ref) {
return AuthService();
});
final currentUserProvider = Provider<User?>((ref) {
final authService = ref.watch(authServiceProvider);
ref.watch(authStateProvider);
return authService.currentUser;
});
final authStateProvider = StreamProvider<AuthState>((ref) {
final authService = ref.watch(authServiceProvider);
return authService.authStateChanges;
});
Go Service Pattern
// Service Interface
type PostService interface {
CreatePost(ctx context.Context, req *CreatePostRequest) (*Post, error)
GetFeed(ctx context.Context, userID string, pagination *Pagination) ([]*Post, error)
LikePost(ctx context.Context, userID, postID string) error
}
// Service Implementation
type postService struct {
postRepo repository.PostRepository
userRepo repository.UserRepository
notifier services.NotificationService
}
func NewPostService(postRepo, userRepo repository.PostRepository, notifier services.NotificationService) PostService {
return &postService{
postRepo: postRepo,
userRepo: userRepo,
notifier: notifier,
}
}
Error Handling Patterns
Flutter Error Handling
// Result Type
class Result<T> {
final T? data;
final String? error;
Result.success(this.data) : error = null;
Result.error(this.error) : data = null;
bool get isSuccess => error == null;
bool get isError => error != null;
}
// Usage
Future<Result<Post>> createPost(CreatePostRequest request) async {
try {
final post = await apiService.createPost(request);
return Result.success(post);
} catch (e) {
return Result.error(e.toString());
}
}
Go Error Handling
// Custom Error Types
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
// Service Error Handling
func (s *postService) CreatePost(ctx context.Context, req *CreatePostRequest) (*Post, error) {
if err := req.Validate(); err != nil {
return nil, ValidationError{Field: "request", Message: err.Error()}
}
post, err := s.postRepo.Create(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to create post: %w", err)
}
return post, nil
}
Testing Strategy
Flutter Testing
Unit Tests
// Example: Post Service Test
void main() {
group('PostService', () {
late MockApiService mockApiService;
late PostService postService;
setUp(() {
mockApiService = MockApiService();
postService = PostService(mockApiService);
});
test('createPost should return post on success', () async {
// Arrange
final mockPost = Post(id: '1', content: 'Test post');
when(mockApiService.createPost(any))
.thenAnswer((_) async => mockPost);
// Act
final result = await postService.createPost(CreatePostRequest(content: 'Test post'));
// Assert
expect(result.isSuccess, true);
expect(result.data?.content, 'Test post');
});
});
}
Widget Tests
void main() {
testWidgets('PostCard displays post content', (WidgetTester tester) async {
// Arrange
final post = Post(id: '1', content: 'Test post', author: User(name: 'Test User'));
// Act
await tester.pumpWidget(
MaterialApp(
home: PostCard(post: post),
),
);
// Assert
expect(find.text('Test post'), findsOneWidget);
expect(find.text('Test User'), findsOneWidget);
});
}
Go Testing
Unit Tests
func TestPostService_CreatePost(t *testing.T) {
tests := []struct {
name string
request *CreatePostRequest
want *Post
wantErr bool
}{
{
name: "valid request",
request: &CreatePostRequest{
UserID: "user123",
Content: "Test post",
},
want: &Post{
ID: "post123",
UserID: "user123",
Content: "Test post",
},
wantErr: false,
},
{
name: "invalid request",
request: &CreatePostRequest{
UserID: "",
Content: "Test post",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := &MockPostRepository{}
service := NewPostService(mockRepo, nil, nil)
if !tt.wantErr {
mockRepo.On("Create", mock.Anything, tt.request).Return(tt.want, nil)
}
got, err := service.CreatePost(context.Background(), tt.request)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
}
mockRepo.AssertExpectations(t)
})
}
}
Performance Optimization
Flutter Performance
Key Optimizations
- Const Constructors: Use
constfor immutable widgets - Lazy Loading: Implement
ListView.builderfor large lists - Image Caching: Use
cached_network_imagefor remote images - State Management: Minimize rebuilds with selective providers
Example: Optimized List View
ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
return PostCard(
key: ValueKey(posts[index].id),
post: posts[index],
);
},
)
Go Performance
Database Optimizations
- Connection Pooling: Configure appropriate pool sizes
- Query Optimization: Use prepared statements and proper indexes
- Caching: Implement Redis for frequently accessed data
Example: Database Configuration
config, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
return nil, err
}
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = time.Hour
config.HealthCheckPeriod = time.Minute * 5
pool, err := pgxpool.NewWithConfig(context.Background(), config)
Security Considerations
Authentication Security
- JWT tokens with proper expiration
- Secure token storage (FlutterSecureStorage)
- Refresh token rotation
- Rate limiting on auth endpoints
Data Protection
- Input validation and sanitization
- SQL injection prevention with parameterized queries
- XSS protection in web views
- CSRF protection for state-changing operations
Privacy Protection
- Data minimization in API responses
- Anonymous analytics collection
- User data export and deletion capabilities
- GDPR compliance considerations
Deployment Architecture
Production Stack
Internet
↓
Nginx (SSL Termination, Static Files)
↓
Go Backend (API, Business Logic)
↓
PostgreSQL (Data, PostGIS)
↓
File System (Uploads) / Cloudflare R2
Environment Configuration
- Development: Local PostgreSQL, mock services
- Staging: Production-like environment with test data
- Production: Full stack with monitoring and backups
Monitoring & Observability
- Application Metrics: Request latency, error rates, user activity
- Infrastructure Metrics: CPU, memory, disk usage
- Database Metrics: Query performance, connection pool status
- Business Metrics: User engagement, content creation rates
Last Updated: January 30, 2026 Version: 1.0 Next Review: February 15, 2026