- TypeScript 90.3%
- CSS 7.5%
- Shell 2.1%
- HTML 0.1%
| deploy | ||
| docs | ||
| scripts | ||
| server | ||
| src | ||
| tests/security | ||
| .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
npm lifecycle-script policy
npm 12 blocks dependency lifecycle scripts unless the project explicitly
approves them. The version-pinned allowScripts entries in package.json
approve only the native tooling required by this lockfile:
better-sqlite3@11.10.0downloads and verifies a compatible prebuilt SQLite binding, or builds the binding from source withnode-gypwhen necessary.esbuild@0.21.5andesbuild@0.28.2select, prepare, and version-check the platform-specific esbuild executables used by Vite andtsx.
When one of these packages changes version, review its lifecycle script and
update the pinned approval instead of adding an unversioned or blanket
approval. npm install-scripts ls should report no blocked required scripts
after installation.
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 seed |
Seed isolated development storage |
npm run seed -- --reset --confirm-dev-reset |
Reset and reseed isolated development storage |
npm run cleanup:test-data -- --confirm-dev-cleanup |
Remove known test data from isolated development storage |
npm run smoke |
Start a temporary isolated API and run smoke checks |
npm run smoke:live |
Run read-only smoke checks against an already-running target |
npm run preview |
Preview production build |
Development commands force NODE_ENV=development; they do not inherit a production mode from .env.
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 postPOST /api/reports— Submit a post/comment report (verified users; rate-limited, visible targets only)GET /api/admin/reports— Review reports (admin only)
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 runs in WAL mode. Storage is selected centrally by server/config.ts:
| Mode | Database default | Uploads default |
|---|---|---|
| Development (default) | data/dev-social.db |
uploads-dev/ |
Production (NODE_ENV=production) |
data/social.db |
uploads/ |
Test (NODE_ENV=test) |
Explicit DATABASE_PATH required |
Explicit UPLOADS_DIR required |
Set DATABASE_PATH and UPLOADS_DIR to override these paths. Relative values
are resolved from the repository root. Non-production processes refuse paths
that resolve to production storage, including symlink aliases. Test mode has no
fallback. Parent directories are created only after configuration passes these
checks.
The isolated smoke harness creates both test paths under a unique
/tmp/refugecloud-smoke.* directory and removes them when it exits:
npm run smoke
To inspect an existing deployed site without starting a server, use:
SMOKE_BASE_URL=https://refugecloud.com npm run smoke:live
Core tables include:
- 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 |
DATABASE_PATH |
mode-dependent | SQLite file; relative paths resolve from repository root |
UPLOADS_DIR |
mode-dependent | Media directory; relative paths resolve from repository root |
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, DATABASE_PATH=data/social.db,
# UPLOADS_DIR=uploads
# 2. Build
npm run build
# 3. Start (production)
npm run start
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
npm run seed -- --reset --confirm-dev-reset
Both commands are restricted to isolated non-production storage. Reset requires the explicit confirmation flag shown above. Production mode or production database/upload paths cause a loud refusal before SQLite is opened.
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:
- Development media is stored in
uploads-dev/by default (gitignored) - Production media is stored in
uploads/when production mode is explicitly selected - Production should use object storage (S3, R2) or CDN
uploads/directory must be backed up- No autoplay on embedded videos