···88DATABASE_URL=postgres://atbb:atbb@localhost:5432/atbb
991010# Web UI configuration
1111-# PORT=3001 (set in web package, or override here)
1111+# WEB_PORT=3001 # set in web package, or override here
1212APPVIEW_URL=http://localhost:3000
13131414# Forum Service Account credentials (for spike and AppView writes)
+2-2
.env.production.example
···114114# AppView API server port (default: 3000)
115115# This is the internal port the appview service listens on.
116116# PORT=3000
117117+# WEB_PORT=3001
117118118118-# Note: The web service also uses PORT (not WEB_PORT) and defaults to 3001.
119119-# In the Docker container, nginx listens on port 80 and proxies to both services.
119119+# Note: In the Docker container, nginx listens on port 80 and proxies to both services.
120120121121# ============================================================================
122122# AT Protocol Features (Optional)
···11import { defineConfig } from "vitest/config";
22+import { config as loadDotenv } from "dotenv";
33+import { resolve } from "node:path";
44+import { existsSync } from "node:fs";
55+66+// Load .env file from monorepo root (for local dev)
77+// Try main repo path first (../../.env from apps/appview)
88+// If not found, try worktree path (../../../../.env from .worktrees/branch/apps/appview)
99+const mainRepoPath = resolve(__dirname, "../../.env");
1010+const worktreePath = resolve(__dirname, "../../../../.env");
1111+const envPath = existsSync(mainRepoPath) ? mainRepoPath : worktreePath;
1212+1313+// Load .env at config time so variables are available when tests run
1414+// dotenv won't override existing environment variables (e.g., from CI)
1515+loadDotenv({ path: envPath });
216317export default defineConfig({
418 test: {
519 environment: "node",
66- // Load .env file before tests via setup file
77- // This allows process.env to work naturally (GitHub Actions vars pass through)
88- setupFiles: ["./vitest.setup.ts"],
920 // Run test files sequentially to avoid database conflicts
1021 // Tests share a single test database and use the same test DIDs
1122 fileParallelism: false,
-14
apps/appview/vitest.setup.ts
···11-import { config } from "dotenv";
22-import { resolve } from "node:path";
33-import { existsSync } from "node:fs";
44-55-// Load .env file from monorepo root (for local dev)
66-// In CI, DATABASE_URL is already set by GitHub Actions workflow
77-// dotenv() won't override existing environment variables
88-99-// Try main repo path first (../../.env from apps/appview)
1010-// If not found, try worktree path (../../../../.env from .worktrees/branch/apps/appview)
1111-const mainRepoPath = resolve(__dirname, "../../.env");
1212-const worktreePath = resolve(__dirname, "../../../../.env");
1313-const envPath = existsSync(mainRepoPath) ? mainRepoPath : worktreePath;
1414-config({ path: envPath });
+7-7
apps/web/src/lib/__tests__/config.test.ts
···1616 return mod.loadConfig();
1717 }
18181919- it("returns default port 3001 when PORT is undefined", async () => {
2020- delete process.env.PORT;
1919+ it("returns default port 3001 when WEB_PORT is undefined", async () => {
2020+ delete process.env.WEB_PORT;
2121 const config = await loadConfig();
2222 expect(config.port).toBe(3001);
2323 });
24242525- it("parses PORT as an integer", async () => {
2626- process.env.PORT = "8080";
2525+ it("parses WEB_PORT as an integer", async () => {
2626+ process.env.WEB_PORT = "8080";
2727 const config = await loadConfig();
2828 expect(config.port).toBe(8080);
2929 expect(typeof config.port).toBe("number");
···3636 });
37373838 it("uses provided environment variables", async () => {
3939- process.env.PORT = "9000";
3939+ process.env.WEB_PORT = "9000";
4040 process.env.APPVIEW_URL = "https://api.atbb.space";
4141 const config = await loadConfig();
4242 expect(config.port).toBe(9000);
4343 expect(config.appviewUrl).toBe("https://api.atbb.space");
4444 });
45454646- it("returns NaN for port when PORT is empty string (?? does not catch empty strings)", async () => {
4747- process.env.PORT = "";
4646+ it("returns NaN for port when WEB_PORT is empty string (?? does not catch empty strings)", async () => {
4747+ process.env.WEB_PORT = "";
4848 const config = await loadConfig();
4949 // Documents a gap: ?? only catches null/undefined, not ""
5050 expect(config.port).toBeNaN();
···221221React Native + Expo cross-platform apps consuming the same `/api/*` endpoints as the web UI. Phased rollout: read-only browse → write/interact → push notifications → offline support & app store release. Full plan in [`docs/mobile-apps-plan.md`](mobile-apps-plan.md).
222222223223### Other Future Work
224224+- **Setup wizard for first-time initialization** — Interactive web-based wizard for administrators to initialize a new forum instance (create forum record on PDS, configure categories, set admin roles). Currently requires manual spike script execution or direct PDS API calls.
224225- Nested/threaded replies
225226- Full-text search (maybe Meilisearch)
226227- User profiles & post history
···11#!/usr/bin/env bash
22-set -euo pipefail
22+# set -euo pipefail
3344# ATB-15 Manual Testing Helper Script
55# Tests membership auto-creation during OAuth login
···4848set +a
49495050# Verify required variables
5151-REQUIRED_VARS=("DATABASE_URL" "FORUM_DID" "PDS_URL")
5151+REQUIRED_VARS=("DATABASE_URL" "FORUM_DID" "PDS_URL" "OAUTH_PUBLIC_URL")
5252for var in "${REQUIRED_VARS[@]}"; do
5353 if [ -z "${!var:-}" ]; then
5454 print_error "Required environment variable $var is not set in .env"
···8686 exit 1
8787fi
88888989-# Check if membership already exists in database
9090-print_step "2" "Checking database for existing membership"
8989+# Ensure forum record exists in database
9090+print_step "2" "Ensuring forum record exists"
9191FORUM_URI="at://${FORUM_DID}/space.atbb.forum.forum/self"
92929393+FORUM_COUNT=$(psql "$DATABASE_URL" -t -c \
9494+ "SELECT COUNT(*) FROM forums WHERE did = '$FORUM_DID' AND rkey = 'self';" \
9595+ 2>/dev/null | tr -d ' ')
9696+9797+if [ "$FORUM_COUNT" -eq 0 ]; then
9898+ print_info "No forum record found. Creating one for testing..."
9999+ psql "$DATABASE_URL" -c \
100100+ "INSERT INTO forums (did, rkey, cid, name, description, indexed_at)
101101+ VALUES ('$FORUM_DID', 'self', 'bafytest123', 'Test Forum', 'Test forum for membership creation', NOW());" \
102102+ 2>/dev/null
103103+ print_success "Created forum record in database"
104104+else
105105+ print_success "Forum record already exists"
106106+fi
107107+108108+# Check if membership already exists in database
109109+print_step "3" "Checking database for existing membership"
110110+93111MEMBERSHIP_COUNT=$(psql "$DATABASE_URL" -t -c \
9494- "SELECT COUNT(*) FROM memberships WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';" \
112112+ "SELECT COUNT(*) FROM memberships WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';" \
95113 2>/dev/null | tr -d ' ')
9611497115if [ "$MEMBERSHIP_COUNT" -gt 0 ]; then
···100118 echo
101119 if [[ $REPLY =~ ^[Yy]$ ]]; then
102120 psql "$DATABASE_URL" -c \
103103- "DELETE FROM memberships WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';"
121121+ "DELETE FROM memberships WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';"
104122 print_success "Deleted existing membership from database"
105123 fi
106124else
···108126fi
109127110128# Instructions for OAuth flow
111111-print_step "3" "OAuth Login Flow"
129129+print_step "4" "OAuth Login Flow"
130130+echo ""
131131+132132+# Construct OAuth login URL
133133+LOGIN_URL="${OAUTH_PUBLIC_URL}/api/auth/login?handle=${TEST_HANDLE}"
134134+135135+echo "Open this URL in your browser to start OAuth login:"
136136+echo ""
137137+echo -e "${GREEN}${LOGIN_URL}${NC}"
138138+echo ""
139139+echo "What will happen:"
140140+echo " 1. Browser redirects to your PDS (${PDS_URL})"
141141+echo " 2. You approve the forum's access request"
142142+echo " 3. PDS redirects back to ${OAUTH_PUBLIC_URL}/api/auth/callback"
143143+echo " 4. Forum creates session and membership record"
144144+echo " 5. Browser redirects to homepage"
112145echo ""
113113-echo "Manual steps:"
114114-echo " 1. Open browser to http://localhost:3001"
115115-echo " 2. Click 'Login' button"
116116-echo " 3. Enter handle: $TEST_HANDLE"
117117-echo " 4. Complete OAuth flow at PDS"
118118-echo " 5. Verify redirect to homepage"
146146+print_info "Tip: Copy the URL above or use 'open \"$LOGIN_URL\"' on macOS"
119147echo ""
120148read -p "Press Enter after completing OAuth login..."
121149122150# Check server logs for membership creation
123123-print_step "4" "Checking server logs"
124124-print_info "Looking for membership creation logs in the last 60 seconds..."
151151+print_step "5" "Checking server logs"
152152+print_info "Check your dev server console for these log events..."
125153126126-# Try to find logs (this assumes logs are in stdout/stderr of dev server)
127154echo ""
128128-echo "Expected log patterns:"
129129-echo " • First login: \"Membership record created\""
130130-echo " • Repeated login: \"Membership already exists\""
131131-echo " • Error case: \"Failed to create membership record - login will proceed\""
155155+echo "Expected log events (JSON formatted):"
156156+echo " • oauth.login.initiated - OAuth flow started"
157157+echo " • oauth.callback.success - User authenticated successfully"
158158+echo " • oauth.callback.membership.created - New membership record created (first login)"
159159+echo " • oauth.callback.membership.exists - Membership already exists (repeated login)"
160160+echo " • oauth.callback.membership.failed - Membership creation failed (error case)"
132161echo ""
133133-print_info "Check your dev server console for these log messages"
162162+print_info "The membership creation happens during the callback, so look for the membership event"
134163135164# Query database for indexed membership
136136-print_step "5" "Checking database for indexed membership"
165165+print_step "6" "Checking database for indexed membership"
137166sleep 2 # Give firehose a moment to index
138167139168MEMBERSHIP_DATA=$(psql "$DATABASE_URL" -c \
140140- "SELECT did, rkey, \"forumUri\", \"joinedAt\", \"createdAt\", \"indexedAt\"
169169+ "SELECT did, rkey, forum_uri, joined_at, created_at, indexed_at
141170 FROM memberships
142142- WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';" \
171171+ WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';" \
143172 2>/dev/null)
144173145174if echo "$MEMBERSHIP_DATA" | grep -q "$TEST_DID"; then
···152181fi
153182154183# Test repeated login
155155-print_step "6" "Test repeated login (no duplicate)"
184184+print_step "7" "Test repeated login (no duplicate)"
185185+echo ""
186186+187187+# Construct logout and login URLs
188188+LOGOUT_URL="${OAUTH_PUBLIC_URL}/api/auth/logout"
189189+190190+echo "First, logout to clear the current session:"
191191+echo ""
192192+echo -e "${GREEN}${LOGOUT_URL}${NC}"
193193+echo ""
194194+print_info "Tip: Open the URL or use 'open \"$LOGOUT_URL\"' on macOS"
156195echo ""
157157-echo "Manual steps:"
158158-echo " 1. Logout from forum (http://localhost:3001/api/auth/logout)"
159159-echo " 2. Login again with same account"
160160-echo " 3. Complete OAuth flow"
161161-echo " 4. Check logs for \"Membership already exists\""
196196+read -p "Press Enter after logging out..."
197197+echo ""
198198+echo "Now login again with the same account:"
199199+echo ""
200200+echo -e "${GREEN}${LOGIN_URL}${NC}"
201201+echo ""
202202+echo "Expected behavior:"
203203+echo " • Login should succeed normally"
204204+echo " • Server logs should show \"Membership already exists\" (not \"created\")"
205205+echo " • No duplicate membership record in database"
162206echo ""
163163-read -p "Press Enter after completing repeated login test..."
207207+read -p "Press Enter after completing repeated login..."
164208165209# Verify no duplicate in database
166210FINAL_COUNT=$(psql "$DATABASE_URL" -t -c \
167167- "SELECT COUNT(*) FROM memberships WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';" \
211211+ "SELECT COUNT(*) FROM memberships WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';" \
168212 2>/dev/null | tr -d ' ')
169213170214if [ "$FINAL_COUNT" -eq 1 ]; then
···172216elif [ "$FINAL_COUNT" -gt 1 ]; then
173217 print_error "FAIL: Found $FINAL_COUNT memberships (duplicates created!)"
174218 psql "$DATABASE_URL" -c \
175175- "SELECT * FROM memberships WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';"
219219+ "SELECT * FROM memberships WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';"
176220 exit 1
177221else
178222 print_error "FAIL: No membership found in database"