January 1, 2025

Architecture Guide

Rox is built with proven design patterns to ensure maintainability, testability, and flexibility.

Core Design Patterns

Repository Pattern

The Repository Pattern abstracts database operations through interfaces, decoupling business logic from data access implementation.

Benefits

  • Testability: Easy to mock repositories for unit testing
  • Flexibility: Switch database implementations without changing business logic
  • Maintainability: Clear separation of concerns

Key Repositories

interface INoteRepository {
  create(note: Note): Promise<Note>;
  findById(id: string): Promise<Note | null>;
  findByUserId(userId: string): Promise<Note[]>;
  update(id: string, note: Partial<Note>): Promise<Note>;
  delete(id: string): Promise<void>;
}

interface IUserRepository {
  create(user: User): Promise<User>;
  findById(id: string): Promise<User | null>;
  findByUsername(username: string): Promise<User | null>;
  update(id: string, user: Partial<User>): Promise<User>;
  delete(id: string): Promise<void>;
}

interface IFileRepository {
  create(file: File): Promise<File>;
  findById(id: string): Promise<File | null>;
  delete(id: string): Promise<void>;
}

Implementation

Repositories are implemented using Drizzle ORM:

export class DrizzleNoteRepository implements INoteRepository {
  constructor(private db: DrizzleDatabase) {}

  async create(note: Note): Promise<Note> {
    const [created] = await this.db
      .insert(notes)
      .values(note)
      .returning();
    return created;
  }

  // ... other methods
}

Adapter Pattern

The Adapter Pattern provides a consistent interface for different storage backends.

Storage Adapters

interface IStorageAdapter {
  upload(file: Buffer, key: string): Promise<string>;
  download(key: string): Promise<Buffer>;
  delete(key: string): Promise<void>;
  getUrl(key: string): string;
}

Local Storage Adapter

export class LocalStorageAdapter implements IStorageAdapter {
  constructor(private basePath: string) {}

  async upload(file: Buffer, key: string): Promise<string> {
    const filePath = path.join(this.basePath, key);
    await fs.writeFile(filePath, file);
    return key;
  }

  getUrl(key: string): string {
    return `/uploads/${key}`;
  }

  // ... other methods
}

S3 Storage Adapter

export class S3StorageAdapter implements IStorageAdapter {
  constructor(
    private s3Client: S3Client,
    private bucketName: string
  ) {}

  async upload(file: Buffer, key: string): Promise<string> {
    await this.s3Client.send(
      new PutObjectCommand({
        Bucket: this.bucketName,
        Key: key,
        Body: file,
      })
    );
    return key;
  }

  getUrl(key: string): string {
    return `https://${this.bucketName}.s3.amazonaws.com/${key}`;
  }

  // ... other methods
}

Dependency Injection

Rox uses Hono's context to inject dependencies based on environment variables.

Configuration

// Setup in middleware
app.use('*', async (c, next) => {
  // Database
  const db = createDatabase(c.env.DB_TYPE);
  c.set('db', db);

  // Repositories
  c.set('noteRepository', new DrizzleNoteRepository(db));
  c.set('userRepository', new DrizzleUserRepository(db));

  // Storage
  const storage = createStorageAdapter(c.env.STORAGE_TYPE);
  c.set('storage', storage);

  await next();
});

Usage in Routes

app.post('/api/notes', async (c) => {
  const noteRepository = c.get('noteRepository');
  const note = await noteRepository.create(data);
  return c.json(note);
});

Project Structure

packages/
├── backend/
│   ├── src/
│   │   ├── adapters/        # Storage adapters
│   │   ├── repositories/    # Database repositories
│   │   ├── services/        # Business logic
│   │   ├── routes/          # API routes
│   │   ├── middleware/      # Middleware
│   │   ├── types/           # TypeScript types
│   │   └── index.ts         # Entry point
│   ├── drizzle/             # Database migrations
│   └── tests/               # Tests
├── frontend/
│   ├── src/
│   │   ├── components/      # React components
│   │   ├── pages/           # Waku pages
│   │   ├── lib/             # Utilities
│   │   ├── atoms/           # Jotai atoms
│   │   └── styles/          # Global styles
│   └── public/              # Static assets
└── shared/
    ├── types/               # Shared TypeScript types
    └── utils/               # Shared utilities

Database Layer

Drizzle ORM

Rox uses Drizzle ORM for type-safe database operations.

Schema Definition

export const users = pgTable('users', {
  id: text('id').primaryKey(),
  username: text('username').notNull().unique(),
  displayName: text('display_name'),
  bio: text('bio'),
  avatarUrl: text('avatar_url'),
  createdAt: timestamp('created_at').defaultNow(),
});

export const notes = pgTable('notes', {
  id: text('id').primaryKey(),
  userId: text('user_id').references(() => users.id),
  text: text('text'),
  visibility: text('visibility'),
  createdAt: timestamp('created_at').defaultNow(),
});

Migrations

# Generate migration
bun run db:generate

# Run migration
bun run db:migrate

# Open Drizzle Studio
bun run db:studio

Multi-Database Support

Rox supports multiple databases through Drizzle's database adapters:

  • PostgreSQL: drizzle-orm/postgres-js
  • MySQL: drizzle-orm/mysql2
  • SQLite: drizzle-orm/better-sqlite3
  • Cloudflare D1: drizzle-orm/d1

API Layer

Hono Framework

Rox uses Hono for its lightweight and fast API server.

Route Definition

const app = new Hono();

app.get('/api/notes/:id', async (c) => {
  const noteRepository = c.get('noteRepository');
  const note = await noteRepository.findById(c.req.param('id'));
  
  if (!note) {
    return c.json({ error: 'Not found' }, 404);
  }
  
  return c.json(note);
});

Middleware

// Authentication middleware
const requireAuth = async (c, next) => {
  const token = c.req.header('Authorization');
  if (!token) {
    return c.json({ error: 'Unauthorized' }, 401);
  }
  
  const user = await verifyToken(token);
  c.set('user', user);
  await next();
};

app.use('/api/notes/*', requireAuth);

Frontend Architecture

Waku (React Server Components)

Rox frontend uses Waku for server-side rendering and React Server Components.

Page Structure

// Server Component
export default async function NotePage({ id }: { id: string }) {
  const note = await fetchNote(id);
  
  return (
    <div>
      <NoteContent note={note} />
      <NoteActions noteId={id} />
    </div>
  );
}

State Management (Jotai)

// atoms/user.ts
export const currentUserAtom = atom<User | null>(null);

// Usage in component
const [user, setUser] = useAtom(currentUserAtom);

React Aria Components

Accessible UI components without styling:

import { Button } from 'react-aria-components';

<Button onPress={() => handleSubmit()}>
  Submit
</Button>

Testing Strategy

Unit Tests

describe('NoteRepository', () => {
  it('should create a note', async () => {
    const repo = new DrizzleNoteRepository(mockDb);
    const note = await repo.create({
      text: 'Hello, world!',
      userId: 'user1',
    });
    
    expect(note.text).toBe('Hello, world!');
  });
});

Integration Tests

describe('POST /api/notes', () => {
  it('should create a note', async () => {
    const res = await app.request('/api/notes', {
      method: 'POST',
      body: JSON.stringify({ text: 'Hello!' }),
      headers: { 'Content-Type': 'application/json' },
    });
    
    expect(res.status).toBe(201);
  });
});

Performance Considerations

Caching

Rox uses Redis (via Dragonfly) for caching:

const cacheService = new CacheService(redis);

// Cache user profiles
await cacheService.set(`user:${userId}`, user, 3600);
const cachedUser = await cacheService.get(`user:${userId}`);

Image Optimization

Images are automatically converted to WebP:

const imageProcessor = new ImageProcessor();
const optimized = await imageProcessor.toWebP(buffer);

Queue System

Background jobs use BullMQ with Dragonfly:

const deliveryQueue = new Queue('delivery', {
  connection: dragonfly,
});

await deliveryQueue.add('deliver-activity', {
  activityId: '123',
  inboxUrl: 'https://example.com/inbox',
});

Security

Input Validation

All inputs are validated using Zod:

const createNoteSchema = z.object({
  text: z.string().min(1).max(3000),
  visibility: z.enum(['public', 'unlisted', 'followers', 'direct']),
});

const data = createNoteSchema.parse(req.body);

Rate Limiting

app.use('/api/*', rateLimiter({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
}));

Next Steps