- TypeScript 89.4%
- CSS 8.7%
- Shell 1.8%
- HTML 0.1%
| deploy | ||
| docs | ||
| scripts | ||
| server | ||
| src | ||
| .env.example | ||
| .gitignore | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
Social Site
A full-service social media platform. No ads. No algorithms. Just people.
Philosophy
- Chronological feeds by default
- User-controlled feed filters
- No algorithmic ranking
- No ads
- No dark patterns
- Modular architecture
Tech Stack
- Backend: Node.js + Express + TypeScript + SQLite (better-sqlite3)
- Frontend: React 18 + TypeScript + Vite
- Auth: JWT tokens (7-day expiry)
- Database: SQLite with WAL mode, foreign keys enabled
Quick Start
npm install
npm run dev
Opens:
- Frontend: http://localhost:5174
- API: http://localhost:3003 (proxied through Vite)
Scripts
| Command | Description |
|---|---|
npm run dev |
Start both API server + Vite dev server |
npm run server |
Start API server only |
npm run client |
Start Vite dev server only |
npm run build |
TypeScript check + Vite production build |
npm run preview |
Preview production build |
API Endpoints
Auth
POST /api/auth/register— Create accountPOST /api/auth/login— Log inGET /api/auth/me— Current userGET /api/auth/providers— Which social providers are configuredGET /api/auth/google— Start Google OAuth loginGET /api/auth/google/callback— Google OAuth callbackGET /api/auth/steam— Start Steam OpenID loginGET /api/auth/steam/callback— Steam OpenID callback
Feed
GET /api/feed?level=everyone|extended|friends|world— Chronological Home feed. Native levels may include user-controlled World Feed injection;worldreturns only approved RSS/podcast items.
Posts
POST /api/posts— Create postGET /api/posts/:id— Get postDELETE /api/posts/:id— Delete post
Users
GET /api/users/:username— User profilePUT /api/users/profile— Update own profileGET /api/users?q=— Search usersGET /api/users/me/games— Current user's game preferences
Follows
POST /api/follows/:userId— FollowDELETE /api/follows/:userId— Unfollow
Likes
POST /api/likes/:postId— LikeDELETE /api/likes/:postId— Unlike
Comments
POST /api/comments/:postId— Add commentGET /api/comments/:postId— Get comments
Reposts
POST /api/reposts/:postId— Repost
Groups
POST /api/groups— Create groupGET /api/groups— List groupsGET /api/groups/:id— Group detail + feedPOST /api/groups/:id/join— JoinPOST /api/groups/:id/leave— Leave
Notifications
GET /api/notifications— List notificationsGET /api/notifications/unread-count— Unread countPOST /api/notifications/read-all— Mark all read
Admin
GET /api/admin/users— List usersPOST /api/admin/users/:id/ban— Ban userPOST /api/admin/users/:id/unban— Unban userDELETE /api/admin/users/:id— Delete user (cascades posts, follows, likes, groups, notifications, etc.)POST /api/admin/users/:id/verify— Verify userPOST /api/admin/users/:id/unverify— Unverify user (not allowed on admins)POST /api/admin/users/:id/role— Change user roleGET /api/admin/posts— List postsPOST /api/admin/posts/:id/hide— Hide postPOST /api/admin/posts/:id/unhide— Unhide postGET /api/admin/reports— List reportsPOST /api/admin/reports— Report a post
Admin user deletion rules:
- Admin cannot delete their own account
- Admin cannot delete the last remaining admin account
- Deleting a user cascades: posts, comments, follows, likes, reactions, notifications, group memberships (and groups they own), reports they filed, game preferences, LFG posts, block/mute rows, OAuth provider links, RSS comments, RSS source blocks
reports.resolved_byis nulled before delete (nullable FK, no cascade)
Games
GET /api/games?q=— Game catalog search/listGET /api/games/:slug— Game detail, LFG, live servers, and opted-in player discoveryPOST /api/games/:slug/profile— Add or update a game on your profileDELETE /api/games/:slug/profile— Remove a game from your profile
Users can list games they play as profile expression, including platform, play style, mic preference, LFG status, favorite status, visibility, and notes. Game Discovery is opt-in: listed games can remain visible on a profile, but same-game player discovery only shows verified, non-banned, opted-in users who expose that game. Profile privacy and block/mute rules still apply. Matching is explicit shared-game matching and filters only, not algorithmic ranking.
Database
SQLite database stored in data/social.db. Tables:
- users, posts, follows, likes, groups_table, group_members, notifications, reports, user_auth_providers
Environment
Copy .env.example to .env and fill in values.
| Variable | Default | Description |
|---|---|---|
PORT |
3003 |
API server port |
JWT_SECRET |
dev secret | JWT signing secret |
SESSION_SECRET |
dev secret | Express session secret for OAuth |
APP_BASE_URL |
http://localhost:3003 |
Backend base URL |
WEB_BASE_URL |
http://localhost:5174 |
Frontend base URL |
GOOGLE_CLIENT_ID |
— | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
— | Google OAuth client secret |
GOOGLE_CALLBACK_URL |
— | Google redirect URI |
STEAM_API_KEY |
— | Steam Web API key |
STEAM_RETURN_URL |
— | Steam OpenID return URL |
Google OAuth Setup
- Go to Google Cloud Console
- Create a project → OAuth consent screen (External, add test users)
- Create OAuth 2.0 Client ID (Web application)
- Add authorized redirect URI:
http://localhost:3003/api/auth/google/callback- (or your APP_BASE_URL + /api/auth/google/callback)
- Copy Client ID and Client Secret to
.env
Steam OpenID Setup
- Get a Steam Web API key at https://steamcommunity.com/dev/apikey
- Set
STEAM_API_KEYin.env - Set
STEAM_RETURN_URLtohttp://localhost:3003/api/auth/steam/callback - No redirect URI registration needed — Steam uses OpenID return URL
Staging / Production Requirements
Before pointing a real domain at this app, complete all of the following.
Required — will fail to start without these in production
| Step | What to do |
|---|---|
JWT_SECRET |
Set to a random 48+ byte hex string. Generate: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" |
SESSION_SECRET |
Same as above, use a different value |
NODE_ENV=production |
The server refuses to start if either secret is missing when NODE_ENV=production |
Required — for secure HTTPS operation
| Step | What to do |
|---|---|
| Use HTTPS | The session cookie uses secure: true in production — HTTP will break OAuth sessions |
TRUST_PROXY=1 |
Set this when running behind Nginx Proxy Manager. Required for rate limiting to see real client IPs |
Update WEB_BASE_URL |
Set to your real frontend domain (e.g. https://yourdomain.com) |
Update APP_BASE_URL |
Set to your real API domain (e.g. https://yourdomain.com or https://api.yourdomain.com) |
Parked — not yet enabled
| Feature | Status |
|---|---|
| Direct video uploads | Disabled (ENABLE_VIDEO_UPLOADS=false). Requires storage/bandwidth planning before enabling. |
| Email verification / password reset | Not implemented. Parked until real domain + email provider are configured. |
| OAuth (Google / Steam) | Works but requires real callback URLs registered with each provider. |
Never do these
- Do not commit
.envto version control - Do not share
JWT_SECRETorSESSION_SECRET - Do not run with the dev fallback secrets in production (
JWT_SECRETempty = server refuses to start)
Quick production checklist
# 1. Copy and fill in secrets
cp .env.example .env
# Edit .env: set JWT_SECRET, SESSION_SECRET, NODE_ENV=production, TRUST_PROXY=1,
# WEB_BASE_URL, APP_BASE_URL
# 2. Build
npm run build
# 3. Start (production)
NODE_ENV=production node dist/server/index.js
LAN Testing
For testing from another machine on the LAN, update .env:
APP_BASE_URL=http://192.168.254.181:3003
WEB_BASE_URL=http://192.168.254.181:5174
GOOGLE_CALLBACK_URL=http://192.168.254.181:3003/api/auth/google/callback
STEAM_RETURN_URL=http://192.168.254.181:3003/api/auth/steam/callback
The server listens on 0.0.0.0 by default, so it's reachable from LAN.
Production note: Set NODE_ENV=production and TRUST_PROXY=1 in .env. The server auto-enables cookie.secure and enforces required secrets. See the Staging / Production Requirements section.
Local LAN OAuth Setup
Google Cloud Console
- Go to Google Cloud Console
- Create a project → Configure OAuth consent screen
- User Type: External
- Add test users (your email)
- Create OAuth 2.0 Client ID:
- Application type: Web application
- Authorized JavaScript origins:
http://192.168.254.181:5174http://192.168.254.181:3003
- Authorized redirect URI:
http://192.168.254.181:3003/api/auth/google/callback
- Copy Client ID and Client Secret into
.env:GOOGLE_CLIENT_ID=123456789-xxxxx.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=GOCSPX-xxxxx
Steam
- Get a Steam Web API key at https://steamcommunity.com/dev/apikey
- Domain:
192.168.254.181(or your LAN IP)
- Domain:
- Add to
.env:STEAM_API_KEY=YOUR_STEAM_KEY - The
STEAM_RETURN_URLshould already be set in.envto:http://192.168.254.181:3003/api/auth/steam/callback
Verify Setup
curl http://localhost:3003/api/auth/providers
# {"google":true,"steam":true} ← both should be true when configured
When not configured, clicking Google or Steam buttons will show:
- "Google login is not configured yet" (in the UI)
- Redirect to login page with error message (from backend)
World Feed (RSS)
The World Feed pulls content from external RSS sources into a chronological feed of excerpts. Every RSS item is clearly labeled with its source name and includes an "Open original" link pointing to the publisher's website.
Philosophy: External content is never presented as native/local content. No ads, no tracking, no algorithmic ranking. Pure chronological order by published date.
Home Feed Controls
Home feed levels control the native social circle:
- Everyone — public native posts from verified users
- Friends of Friends — people you follow plus extended circle
- Just Friends — your posts and people you follow
- Approved World Feeds — only approved RSS/podcast items
The World Feed on Home preference controls whether approved RSS/podcast items are mixed into normal native Home feeds:
- Off — only native posts appear
- Few — occasionally adds approved RSS/podcast items
- Balanced — adds more approved RSS/podcast items
Injected external items remain visibly labeled as World Feed/RSS/podcast content and are rendered separately from native posts. RSS source blocking applies to Home injection. Native block/mute still applies to native posts. No engagement data, ads, boosted posts, or algorithmic ranking are used.
How It Works
- Admins add RSS sources via the Admin Dashboard → RSS Sources tab
- Admins manually fetch sources ("Fetch" or "Fetch All Active")
- RSS items are parsed, sanitized (scripts/stripped), and stored as excerpts
- Duplicate items are automatically skipped (matched by source + GUID)
- The World Feed displays items chronologically at
/world
Public Access
The World Feed at /world is publicly accessible without an account. Unauthenticated visitors can:
- Browse all RSS items chronologically
- Filter by category and source
- Read existing local discussions/comments
- Click "Open original" links
Unauthenticated visitors cannot:
- Post comments (login required)
- Block/unblock RSS sources (login required)
- Access any account-required features
A "Log in to join the discussion or customize your sources" prompt appears near interaction areas for logged-out visitors.
API Routes
| Endpoint | Access | Description |
|---|---|---|
GET /api/world-feed |
Public | World Feed items (supports ?sourceId=&category=&limit=&offset=) |
GET /api/world-feed/sources |
Public | Active sources and categories |
GET /api/admin/rss/sources |
Admin | List all RSS sources |
POST /api/admin/rss/sources |
Admin | Add a new RSS source |
PATCH /api/admin/rss/sources/:id |
Admin | Update source (name, URL, active status, etc.) |
POST /api/admin/rss/sources/:id/fetch |
Admin | Fetch a single source now |
POST /api/admin/rss/fetch-all |
Admin | Fetch all active sources now |
Seed Data
npm run seed # Seeds demo data (safe — skips if users already exist)
npm run seed -- --reset # Wipes and reseeds fresh demo data
Demo accounts created by seed:
alice/demo1234bob/demo1234admin/admin1234
Seed also creates follows, posts, likes, comments, groups, notifications, and optional RSS sources (Ars Technica, NASA).
Gaming Source Bundle
The seed script includes 6 gaming RSS sources covering PC gaming, console news, industry updates, and platform blogs. All gaming feeds are external RSS items — they are never presented as native/local content.
Included gaming sources (all verified working):
| Source | Category | Coverage |
|---|---|---|
| PC Gamer | Gaming / PC | PC gaming news, reviews, hardware |
| Rock Paper Shotgun | Gaming / PC | PC gaming culture, reviews, opinion |
| Eurogamer | Gaming | Multi-platform gaming news, reviews |
| Nintendo Life | Gaming / Console | Nintendo news, reviews, eShop |
| GamesIndustry.biz | Gaming / Industry | Game industry business, development |
| Steam Blog | Gaming / Platform | Official Steam platform updates |
U.S.-sourced Gaming sources:
| Source | Category |
|---|---|
| IGN | Gaming |
| GameSpot | Gaming |
| Polygon | Gaming |
| Kotaku | Gaming |
| Destructoid | Gaming |
| Ars Technica Gaming | Gaming |
| Xbox Wire | Gaming / Platform |
| PlayStation Blog | Gaming / Platform |
Tech / AI / Hardware sources:
| Source | Category |
|---|---|
| Ars Technica | Tech |
| The Verge | Tech |
| Hacker News | Tech |
| GitHub Blog | Tech |
| Mozilla Blog | Tech |
| NVIDIA Blog | Tech / AI |
| Raspberry Pi | Tech / Hardware |
Skipped sources (no working RSS found):
- NVIDIA Newsroom (XML parse error)
- AMD Blog (XML parse error)
- Intel Newsroom (404)
- Game Informer (no RSS feed)
- Giant Bomb (no RSS feed)
Important notes:
- All items show source name badge, category, and "Open original →" link
- Admins can deactivate or remove any source from the Admin RSS panel
- Feed URLs may change — admins can edit URLs in the admin panel
- Only excerpts/snippets are stored (max 2000 chars), with attribution
Media Uploads
Enabled now:
- Image uploads (JPG, PNG, GIF, WebP) — max 5MB
- YouTube/external video embeds — normal URLs, shorts, youtu.be links
Intentionally disabled:
- SVG uploads are disabled for safety. Use JPG, PNG, GIF, or WebP.
- Direct video uploads (
ENABLE_VIDEO_UPLOADS=false) - Can be enabled later with
ENABLE_VIDEO_UPLOADS=true - Future video hosting requires serious storage/bandwidth planning
Feature flags (.env):
| Variable | Default |
|---|---|
ENABLE_IMAGE_UPLOADS |
true |
ENABLE_EXTERNAL_VIDEO_EMBEDS |
true |
ENABLE_VIDEO_UPLOADS |
false |
MAX_IMAGE_UPLOAD_MB |
5 |
MAX_AVATAR_UPLOAD_MB |
2 |
MAX_VIDEO_UPLOAD_MB |
250 |
Future video support: The post_media table already supports media_type='video' with columns for duration_seconds, thumbnail_url, processing_status. When video uploads are enabled, no schema changes needed.
Production notes:
- Media files stored in
uploads/directory (gitignored) - Production should use object storage (S3, R2) or CDN
uploads/directory must be backed up- No autoplay on embedded videos