commit 6bd06ff2c841eb9ebbe7cf701bc8c1764fb34bc6 Author: Gabriel Kaszewski Date: Fri Sep 5 17:13:31 2025 +0200 Add initial project configuration files including environment variables, Docker Compose setup, API design, database schema, and Nginx configuration diff --git a/.env b/.env new file mode 100644 index 0000000..a56f5fe --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +POSTGRES_USER=thoughts_user +POSTGRES_PASSWORD=postgres +POSTGRES_DB=thoughts_db \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df25325 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +backend-codebase.txt +frontend-codebase.txt \ No newline at end of file diff --git a/API Design.md b/API Design.md new file mode 100644 index 0000000..3527b46 --- /dev/null +++ b/API Design.md @@ -0,0 +1,165 @@ +# **Thoughts \- API Design (Version 1\)** + +## **1\. Overview** + +This document specifies the RESTful API for the Thoughts platform. + +* **Base URL:** /api/v1 +* **Data Format:** All requests and responses will be in JSON format. +* **Authentication:** The API uses two primary methods for authentication: + 1. **JWT (JSON Web Tokens):** For the official web client. The POST /api/v1/auth/login endpoint returns a short-lived JWT. This token must be included in the Authorization: Bearer \ header for all subsequent authenticated requests. + 2. **API Keys:** For third-party applications. Users can generate long-lived API keys. These keys must be included in the Authorization: ApiKey \ header. + +## **2\. API Endpoints** + +### **Auth Endpoints** + +**POST /auth/register** + +* **Description:** Creates a new user account. +* **Authentication:** Public. +* **Request Body:** + { + "username": "frutiger", + "email": "aero@example.com", + "password": "strongpassword123" + } + +* **Success Response:** 201 Created with the new User object (password omitted). +* **Error Responses:** 400 Bad Request (invalid input), 409 Conflict (username or email already exists). + +**POST /auth/login** + +* **Description:** Authenticates a user and returns a JWT. +* **Authentication:** Public. +* **Request Body:** + { + "username": "frutiger", + "password": "strongpassword123" + } + +* **Success Response:** 200 OK with a JWT. + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + +* **Error Responses:** 400 Bad Request, 401 Unauthorized. + +### **User & Profile Endpoints** + +**GET /users/{username}** + +* **Description:** Retrieves the public profile of a user. +* **Authentication:** Public. +* **Success Response:** 200 OK with a public User object. + +**GET /users/me** + +* **Description:** Retrieves the full profile of the currently authenticated user (including private details like email). +* **Authentication:** Required (JWT). +* **Success Response:** 200 OK with the full User object. + +**PUT /users/me** + +* **Description:** Updates the profile of the currently authenticated user. +* **Authentication:** Required (JWT). +* **Request Body:** + { + "displayName": "Frutiger Aero Fan", + "bio": "Est. 2004", + "avatarUrl": "https://...", + "headerUrl": "https://...", + "customCss": "body { background: blue; }", + "topFriends": \["username1", "username2"\] + } + +* **Success Response:** 200 OK with the updated User object. +* **Error Responses:** 400 Bad Request. + +### **Thoughts (Posts) Endpoints** + +**POST /thoughts** + +* **Description:** Creates a new thought. +* **Authentication:** Required (JWT or API Key). +* **Request Body:** + { + "content": "This is my first thought\! \#welcome" + } + +* **Success Response:** 201 Created with the new Thought object. +* **Error Responses:** 400 Bad Request (e.g., content \> 128 chars). + +**GET /users/{username}/thoughts** + +* **Description:** Retrieves all thoughts for a specific user, paginated. +* **Authentication:** Public. +* **Success Response:** 200 OK with an array of Thought objects. + +**DELETE /thoughts/{id}** + +* **Description:** Deletes a thought. The user must be the author. +* **Authentication:** Required (JWT or API Key). +* **Success Response:** 204 No Content. +* **Error Responses:** 403 Forbidden, 404 Not Found. + +### **Social Endpoints** + +**POST /users/{username}/follow** + +* **Description:** Follows a user. +* **Authentication:** Required (JWT). +* **Success Response:** 204 No Content. +* **Error Responses:** 404 Not Found, 409 Conflict (already following). + +**DELETE /users/{username}/follow** + +* **Description:** Unfollows a user. +* **Authentication:** Required (JWT). +* **Success Response:** 204 No Content. +* **Error Responses:** 404 Not Found. + +**GET /feed** + +* **Description:** Retrieves the main feed for the authenticated user, paginated. +* **Authentication:** Required (JWT). +* **Success Response:** 200 OK with an array of Thought objects from followed users. + +### **Discovery Endpoints** + +**GET /tags/popular** + +* **Description:** Retrieves a list of currently popular tags. +* **Authentication:** Public. +* **Success Response:** 200 OK with an array of tag strings. + +**GET /tags/{tagName}** + +* **Description:** Retrieves a feed of all thoughts with a specific tag, paginated. +* **Authentication:** Public. +* **Success Response:** 200 OK with an array of Thought objects. + +## **3\. Data Models** + +**User Object (Public)** + +{ + "username": "frutiger", + "displayName": "Frutiger Aero Fan", + "bio": "Est. 2004", + "avatarUrl": "https://...", + "headerUrl": "https://...", + "customCss": "body { background: blue; }", + "topFriends": \["username1", "username2"\], + "joinedAt": "2024-01-01T12:00:00Z" +} + +**Thought Object** + +{ + "id": "uuid-v4-string", + "authorUsername": "frutiger", + "content": "This is my first thought\! \#welcome", + "tags": \["welcome"\], + "createdAt": "2024-01-01T12:01:00Z" +} diff --git a/Database schema.md b/Database schema.md new file mode 100644 index 0000000..6685480 --- /dev/null +++ b/Database schema.md @@ -0,0 +1,114 @@ +# **Thoughts \- Database Schema (PostgreSQL)** + +## **1\. Overview** + +This document outlines the table structure for the Thoughts platform using PostgreSQL. The design uses UUIDs for primary keys to facilitate decentralization and prevent enumeration attacks. All timestamps are stored with time zones (TIMESTAMPTZ). + +## **2\. Schema Diagram (ERD)** + +\+-------------+ \+--------------+ \+--------------+ +| users |\<--+--| thoughts |---+--|\> thought\_tags | +\+-------------+ | \+--------------+ | \+--------------+ + | | | ^ + | | | | + | | \+--------------+ | \+--------------+ + \+--------+--+--|\> follows |\<--+-+--| tags | + | | \+--------------+ | \+--------------+ + | | | + v | | +\+-------------+ | | +| top\_friends |\<-+ | +\+-------------+ | + | | + v | +\+-------------+ | +| api\_keys |\<--------------------------+ +\+-------------+ + +*(Note: Arrows denote foreign key relationships)* + +## **3\. Table Definitions** + +### **users** + +Stores user account and profile information. + +| Column Name | Data Type | Constraints | Description | +| :---- | :---- | :---- | :---- | +| id | UUID | PRIMARY KEY, DEFAULT gen\_random\_uuid() | Unique identifier for the user. | +| username | VARCHAR(32) | NOT NULL, UNIQUE | The user's handle. | +| email | VARCHAR(255) | NOT NULL, UNIQUE | The user's email address. | +| password\_hash | TEXT | NOT NULL | Hashed password (using Argon2 or bcrypt). | +| display\_name | VARCHAR(50) | NULL | User's public display name. | +| bio | VARCHAR(160) | NULL | User's public biography. | +| avatar\_url | TEXT | NULL | URL to the user's avatar image. | +| header\_url | TEXT | NULL | URL to the user's header image. | +| custom\_css | TEXT | NULL | User's custom profile CSS. | +| created\_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Timestamp of account creation. | +| updated\_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Timestamp of the last profile update. | + +### **thoughts** + +Stores the content of each post. + +| Column Name | Data Type | Constraints | Description | +| :---- | :---- | :---- | :---- | +| id | UUID | PRIMARY KEY, DEFAULT gen\_random\_uuid() | Unique identifier for the thought. | +| user\_id | UUID | NOT NULL, REFERENCES users(id) | The ID of the authoring user. | +| content | VARCHAR(128) | NOT NULL | The text content of the thought. | +| created\_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Timestamp of when the thought was posted. | + +### **follows** + +A join table representing the follower/following relationship. + +| Column Name | Data Type | Constraints | Description | +| :---- | :---- | :---- | :---- | +| follower\_id | UUID | NOT NULL, REFERENCES users(id) | The user who is initiating the follow. | +| following\_id | UUID | NOT NULL, REFERENCES users(id) | The user who is being followed. | +| | | PRIMARY KEY (follower\_id, following\_id) | Ensures a user can't follow someone twice. | + +### **top\_friends** + +Stores the ordered list of a user's "Top Friends". + +| Column Name | Data Type | Constraints | Description | +| :---- | :---- | :---- | :---- | +| user\_id | UUID | NOT NULL, REFERENCES users(id) | The owner of this "Top Friends" list. | +| friend\_id | UUID | NOT NULL, REFERENCES users(id) | The user being displayed as a friend. | +| position | SMALLINT | NOT NULL | The order (1-8) of the friend on the list. | +| | | PRIMARY KEY (user\_id, friend\_id) | Ensures a user can't be in the list twice. | +| | | UNIQUE (user\_id, position) | Ensures positions are not duplicated. | + +### **tags and thought\_tags (for hashtags)** + +* **tags**: Stores unique tag names. +* **thought\_tags**: A join table linking thoughts to tags. + +#### **tags** + +| Column Name | Data Type | Constraints | Description | +| :---- | :---- | :---- | :---- | +| id | SERIAL | PRIMARY KEY | Unique ID for the tag. | +| name | VARCHAR(50) | NOT NULL, UNIQUE | The tag name (e.g., "welcome"). | + +#### **thought\_tags** + +| Column Name | Data Type | Constraints | Description | +| :---- | :---- | :---- | :---- | +| thought\_id | UUID | NOT NULL, REFERENCES thoughts(id) | The ID of the thought. | +| tag\_id | INTEGER | NOT NULL, REFERENCES tags(id) | The ID of the tag. | +| | | PRIMARY KEY (thought\_id, tag\_id) | Prevents duplicate tags per post. | + +### **api\_keys** + +Stores hashed API keys for users. + +| Column Name | Data Type | Constraints | Description | +| :---- | :---- | :---- | :---- | +| id | UUID | PRIMARY KEY, DEFAULT gen\_random\_uuid() | Unique identifier for the API key. | +| user\_id | UUID | NOT NULL, REFERENCES users(id) | The user who owns this key. | +| key\_hash | TEXT | NOT NULL, UNIQUE | The hashed value of the API key. | +| name | VARCHAR(50) | NOT NULL | A user-provided name for the key. | +| created\_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Timestamp of when the key was created. | + diff --git a/codebase-prompt.txt b/codebase-prompt.txt new file mode 100644 index 0000000..a0514ba --- /dev/null +++ b/codebase-prompt.txt @@ -0,0 +1,2 @@ +uvx files-to-prompt thoughts-backend -e toml -e rs -e md --ignore "*target" -o backend-codebase.txt +uvx files-to-prompt thoughts-frontend -o frontend-codebase.txt --ignore "*node_modules" --ignore "*.lock" \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..2eba845 --- /dev/null +++ b/compose.yml @@ -0,0 +1,55 @@ +services: + database: + image: postgres:15-alpine + container_name: thoughts-db + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5433:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + container_name: thoughts-backend + build: + context: ./thoughts-backend + dockerfile: Dockerfile + restart: unless-stopped + env_file: + - .env + depends_on: + database: + condition: service_healthy + + frontend: + container_name: thoughts-frontend + build: + context: ./thoughts-frontend + dockerfile: Dockerfile + restart: unless-stopped + depends_on: + - backend + + proxy: + container_name: thoughts-proxy + image: nginx:stable-alpine + restart: unless-stopped + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf + depends_on: + - frontend + - backend + +volumes: + postgres_data: + driver: local diff --git a/foundation.md b/foundation.md new file mode 100644 index 0000000..497d585 --- /dev/null +++ b/foundation.md @@ -0,0 +1,138 @@ +Project Thoughts: Foundational Plan & System Architecture + +1. Vision & Core Philosophy + Project Vision: To create a decentralized social media platform that prioritizes genuine user connection, creative self-expression, and a user-controlled experience. Thoughts is a deliberate departure from algorithm-driven feeds and corporate-controlled online spaces. + +Core Philosophy: We are building a digital "third place" reminiscent of the early internet, where the focus is on community and individuality. The platform's aesthetic and function are guided by the "Frutiger Aero" design language—optimistic, humanistic, and clear. + +Tagline: Your Space, Your Friends, Your Feed. + +2. Guiding Principles & Rules to Follow + These are the non-negotiable rules that will guide all development and feature decisions. + +Chronological Above All: The main feed will always be in reverse chronological order. There will be no algorithmic sorting or "while you were away" features. + +User in Control: Users, not the platform, decide what they see by choosing who to follow. There will be no algorithmic content or user suggestions. + +Radical Self-Expression: Profiles are a canvas. Users will be given significant freedom to customize the look and feel of their personal space. + +Open and Federated: The platform must be a citizen of the Fediverse. ActivityPub integration is a primary, not secondary, feature. + +Performance by Default: The user experience should be fast and lightweight, minimizing client-side JavaScript and optimizing for quick page loads. + +API-First Design: The backend is the core service. The frontend is simply the first and primary client of a well-documented, public-facing API. This ensures that third-party developers have the same power as the official web client. + +3. System Architecture + This section details the technical structure of the platform. Your concern about running a Rust backend with a Next.js frontend is a common one. The standard and most effective solution is to run them as two separate services that communicate with each other, orchestrated by a reverse proxy. + +3.1. Architecture Diagram (High Level) ++----------------+ +---------------------+ +------------------------+ +-------------------+ +| User's | <--> | Reverse Proxy | <--> | Next.js Frontend | | | +| Browser | | (Nginx / Caddy) | | (Web Server) | | | ++----------------+ +---------------------+ +------------------------+ | PostgreSQL | +| | | Database | +| (Routes /api/\*) | | | +| | +-------------------+ +v | ++------------------------+ <--------------------------------------+ +| Rust Backend | +| (API & ActivityPub) | ++------------------------+ + +3.2. Component Breakdown +Frontend: Next.js + +Role: Acts as the primary client for the Rust API. It is responsible for rendering the user interface. + +Justification: Using Next.js for Server-Side Rendering (SSR) gives us the best of both worlds: fast initial page loads with minimal client-side JavaScript (fulfilling the "lightweight" requirement) and a modern, component-based development experience. It will handle user sessions and render pages by fetching data from the Rust API during the request-response cycle on the server. + +Backend: Rust + +Role: The core of the application. It's a stateless API server that handles all business logic, user authentication, database operations, and ActivityPub federation logic. + +Recommended Framework: axum. It is modern, modular, and integrates seamlessly with the tokio ecosystem, making it a robust choice for building high-performance APIs. + +API Specification: We will use the OpenAPI 3.0 standard to define and document the REST API. + +Database: PostgreSQL + +Role: The single source of truth for all user data, posts, follows, etc. + +Justification: PostgreSQL is a powerful, reliable, and open-source relational database with excellent support in the Rust ecosystem (via libraries like sqlx and diesel). It provides the structure needed for a social platform. + +Deployment: Docker & Docker Compose + +Role: To containerize each component (Frontend, Backend, Database, Reverse Proxy) for consistent, reproducible, and easy deployments. + +docker-compose.yml Structure: The final docker-compose.yml file will define four services: + +thoughts-backend: Builds and runs the Rust application. + +thoughts-frontend: Builds and runs the Next.js application. + +database: Runs the official PostgreSQL image, with a persistent volume for data. + +proxy: Runs an Nginx or Caddy container configured to route traffic. Requests to yourdomain.com/api/\* will be forwarded to the Rust service, and all other requests will go to the Next.js service. + +4. Features & Acceptance Criteria + This outlines the minimum viable product (MVP) features. + +Feature + +Description + +Acceptance Criteria + +User Accounts + +Users can sign up, log in, log out. Usernames are unique. + +I can successfully create an account. I can log in with my credentials and am issued a session. I can log out, which invalidates my session. + +Customizable Profiles + +Each user has a public profile page (/username). Profiles have a display name, bio, avatar, header, and a "Top Friends" list. + +I can edit my profile details. I can upload an avatar and header image. I can select up to 8 other users to display in my "Top Friends" list. + +Profile CSS + +A field in the profile settings allows users to input custom CSS to style their profile page. This CSS is sanitized to prevent security risks. + +I can add custom CSS to my profile. When another user visits my profile, they see my custom styling applied. Malicious CSS (e.g., url() with external scripts) is stripped out. + +Posting Thoughts + +Users can publish short text posts (max 128 characters) to their profile. Posts can include hashtags (e.g., #frutigeraero). + +I can publish a post. It appears on my profile and in the feeds of my followers. Hashtags are clickable links. I receive an error if my post is over 128 characters. + +Following System + +Users can follow and unfollow other users on the platform. + +When I visit a user's profile, I see a "Follow" button. Clicking it adds them to my follow list. Clicking "Unfollow" removes them. + +The Main Feed + +The homepage for a logged-in user. It displays posts from all followed users in reverse chronological order. + +My feed contains posts from everyone I follow. The newest post is always at the top. The feed contains no posts from users I don't follow. + +Tag Discovery + +A "Popular Tags" section on the main page lists tags that are currently trending. Clicking a tag shows a public feed of all posts with that tag. + +The popular tags list updates based on recent post activity. Clicking a tag takes me to a page showing all posts with that tag, sorted chronologically. + +ActivityPub (MVP) + +A user's profile is exposed as an ActivityPub actor (e.g., @username@thoughts.social). + +A user on another Fediverse platform (e.g., Mastodon) can search for my profile and follow me. My new posts are federated and appear on their timeline. + +Public API (MVP) + +A simple, token-based API for publishing thoughts. + +I can generate an API key from my settings. Using this key and curl or another tool, I can successfully publish a "thought" to my profile. API documentation is available. diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..51bb892 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,37 @@ +upstream frontend { + server frontend:3000; +} + +upstream backend { + server backend:8000; +} + +server { + listen 80; + server_name localhost; + + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + send_timeout 300s; + + location /api/ { + rewrite /api/(.*) /$1 break; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://backend; + } + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://frontend; + } +}