···8DATABASE_URL=postgres://atbb:atbb@localhost:5432/atbb
910# Web UI configuration
11-# PORT=3001 (set in web package, or override here)
12APPVIEW_URL=http://localhost:3000
1314# Forum Service Account credentials (for spike and AppView writes)
···8DATABASE_URL=postgres://atbb:atbb@localhost:5432/atbb
910# Web UI configuration
11+# WEB_PORT=3001 # set in web package, or override here
12APPVIEW_URL=http://localhost:3000
1314# Forum Service Account credentials (for spike and AppView writes)
+2-2
.env.production.example
···114# AppView API server port (default: 3000)
115# This is the internal port the appview service listens on.
116# PORT=3000
0117118-# Note: The web service also uses PORT (not WEB_PORT) and defaults to 3001.
119-# In the Docker container, nginx listens on port 80 and proxies to both services.
120121# ============================================================================
122# AT Protocol Features (Optional)
···114# AppView API server port (default: 3000)
115# This is the internal port the appview service listens on.
116# PORT=3000
117+# WEB_PORT=3001
118119+# Note: In the Docker container, nginx listens on port 80 and proxies to both services.
0120121# ============================================================================
122# AT Protocol Features (Optional)
···1import { defineConfig } from "vitest/config";
0000000000000023export default defineConfig({
4 test: {
5 environment: "node",
6- // Load .env file before tests via setup file
7- // This allows process.env to work naturally (GitHub Actions vars pass through)
8- setupFiles: ["./vitest.setup.ts"],
9 // Run test files sequentially to avoid database conflicts
10 // Tests share a single test database and use the same test DIDs
11 fileParallelism: false,
···1import { defineConfig } from "vitest/config";
2+import { config as loadDotenv } from "dotenv";
3+import { resolve } from "node:path";
4+import { existsSync } from "node:fs";
5+6+// Load .env file from monorepo root (for local dev)
7+// Try main repo path first (../../.env from apps/appview)
8+// If not found, try worktree path (../../../../.env from .worktrees/branch/apps/appview)
9+const mainRepoPath = resolve(__dirname, "../../.env");
10+const worktreePath = resolve(__dirname, "../../../../.env");
11+const envPath = existsSync(mainRepoPath) ? mainRepoPath : worktreePath;
12+13+// Load .env at config time so variables are available when tests run
14+// dotenv won't override existing environment variables (e.g., from CI)
15+loadDotenv({ path: envPath });
1617export default defineConfig({
18 test: {
19 environment: "node",
00020 // Run test files sequentially to avoid database conflicts
21 // Tests share a single test database and use the same test DIDs
22 fileParallelism: false,
-14
apps/appview/vitest.setup.ts
···1-import { config } from "dotenv";
2-import { resolve } from "node:path";
3-import { existsSync } from "node:fs";
4-5-// Load .env file from monorepo root (for local dev)
6-// In CI, DATABASE_URL is already set by GitHub Actions workflow
7-// dotenv() won't override existing environment variables
8-9-// Try main repo path first (../../.env from apps/appview)
10-// If not found, try worktree path (../../../../.env from .worktrees/branch/apps/appview)
11-const mainRepoPath = resolve(__dirname, "../../.env");
12-const worktreePath = resolve(__dirname, "../../../../.env");
13-const envPath = existsSync(mainRepoPath) ? mainRepoPath : worktreePath;
14-config({ path: envPath });
···00000000000000
+7-7
apps/web/src/lib/__tests__/config.test.ts
···16 return mod.loadConfig();
17 }
1819- it("returns default port 3001 when PORT is undefined", async () => {
20- delete process.env.PORT;
21 const config = await loadConfig();
22 expect(config.port).toBe(3001);
23 });
2425- it("parses PORT as an integer", async () => {
26- process.env.PORT = "8080";
27 const config = await loadConfig();
28 expect(config.port).toBe(8080);
29 expect(typeof config.port).toBe("number");
···36 });
3738 it("uses provided environment variables", async () => {
39- process.env.PORT = "9000";
40 process.env.APPVIEW_URL = "https://api.atbb.space";
41 const config = await loadConfig();
42 expect(config.port).toBe(9000);
43 expect(config.appviewUrl).toBe("https://api.atbb.space");
44 });
4546- it("returns NaN for port when PORT is empty string (?? does not catch empty strings)", async () => {
47- process.env.PORT = "";
48 const config = await loadConfig();
49 // Documents a gap: ?? only catches null/undefined, not ""
50 expect(config.port).toBeNaN();
···16 return mod.loadConfig();
17 }
1819+ it("returns default port 3001 when WEB_PORT is undefined", async () => {
20+ delete process.env.WEB_PORT;
21 const config = await loadConfig();
22 expect(config.port).toBe(3001);
23 });
2425+ it("parses WEB_PORT as an integer", async () => {
26+ process.env.WEB_PORT = "8080";
27 const config = await loadConfig();
28 expect(config.port).toBe(8080);
29 expect(typeof config.port).toBe("number");
···36 });
3738 it("uses provided environment variables", async () => {
39+ process.env.WEB_PORT = "9000";
40 process.env.APPVIEW_URL = "https://api.atbb.space";
41 const config = await loadConfig();
42 expect(config.port).toBe(9000);
43 expect(config.appviewUrl).toBe("https://api.atbb.space");
44 });
4546+ it("returns NaN for port when WEB_PORT is empty string (?? does not catch empty strings)", async () => {
47+ process.env.WEB_PORT = "";
48 const config = await loadConfig();
49 // Documents a gap: ?? only catches null/undefined, not ""
50 expect(config.port).toBeNaN();
···221React 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).
222223### Other Future Work
0224- Nested/threaded replies
225- Full-text search (maybe Meilisearch)
226- User profiles & post history
···221React 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).
222223### Other Future Work
224+- **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.
225- Nested/threaded replies
226- Full-text search (maybe Meilisearch)
227- User profiles & post history
···1#!/usr/bin/env bash
2-set -euo pipefail
34# ATB-15 Manual Testing Helper Script
5# Tests membership auto-creation during OAuth login
···48set +a
4950# Verify required variables
51-REQUIRED_VARS=("DATABASE_URL" "FORUM_DID" "PDS_URL")
52for var in "${REQUIRED_VARS[@]}"; do
53 if [ -z "${!var:-}" ]; then
54 print_error "Required environment variable $var is not set in .env"
···86 exit 1
87fi
8889-# Check if membership already exists in database
90-print_step "2" "Checking database for existing membership"
91FORUM_URI="at://${FORUM_DID}/space.atbb.forum.forum/self"
9200000000000000000093MEMBERSHIP_COUNT=$(psql "$DATABASE_URL" -t -c \
94- "SELECT COUNT(*) FROM memberships WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';" \
95 2>/dev/null | tr -d ' ')
9697if [ "$MEMBERSHIP_COUNT" -gt 0 ]; then
···100 echo
101 if [[ $REPLY =~ ^[Yy]$ ]]; then
102 psql "$DATABASE_URL" -c \
103- "DELETE FROM memberships WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';"
104 print_success "Deleted existing membership from database"
105 fi
106else
···108fi
109110# Instructions for OAuth flow
111-print_step "3" "OAuth Login Flow"
000000000000000112echo ""
113-echo "Manual steps:"
114-echo " 1. Open browser to http://localhost:3001"
115-echo " 2. Click 'Login' button"
116-echo " 3. Enter handle: $TEST_HANDLE"
117-echo " 4. Complete OAuth flow at PDS"
118-echo " 5. Verify redirect to homepage"
119echo ""
120read -p "Press Enter after completing OAuth login..."
121122# Check server logs for membership creation
123-print_step "4" "Checking server logs"
124-print_info "Looking for membership creation logs in the last 60 seconds..."
125126-# Try to find logs (this assumes logs are in stdout/stderr of dev server)
127echo ""
128-echo "Expected log patterns:"
129-echo " • First login: \"Membership record created\""
130-echo " • Repeated login: \"Membership already exists\""
131-echo " • Error case: \"Failed to create membership record - login will proceed\""
00132echo ""
133-print_info "Check your dev server console for these log messages"
134135# Query database for indexed membership
136-print_step "5" "Checking database for indexed membership"
137sleep 2 # Give firehose a moment to index
138139MEMBERSHIP_DATA=$(psql "$DATABASE_URL" -c \
140- "SELECT did, rkey, \"forumUri\", \"joinedAt\", \"createdAt\", \"indexedAt\"
141 FROM memberships
142- WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';" \
143 2>/dev/null)
144145if echo "$MEMBERSHIP_DATA" | grep -q "$TEST_DID"; then
···152fi
153154# Test repeated login
155-print_step "6" "Test repeated login (no duplicate)"
0000000000156echo ""
157-echo "Manual steps:"
158-echo " 1. Logout from forum (http://localhost:3001/api/auth/logout)"
159-echo " 2. Login again with same account"
160-echo " 3. Complete OAuth flow"
161-echo " 4. Check logs for \"Membership already exists\""
00000162echo ""
163-read -p "Press Enter after completing repeated login test..."
164165# Verify no duplicate in database
166FINAL_COUNT=$(psql "$DATABASE_URL" -t -c \
167- "SELECT COUNT(*) FROM memberships WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';" \
168 2>/dev/null | tr -d ' ')
169170if [ "$FINAL_COUNT" -eq 1 ]; then
···172elif [ "$FINAL_COUNT" -gt 1 ]; then
173 print_error "FAIL: Found $FINAL_COUNT memberships (duplicates created!)"
174 psql "$DATABASE_URL" -c \
175- "SELECT * FROM memberships WHERE did = '$TEST_DID' AND \"forumUri\" = '$FORUM_URI';"
176 exit 1
177else
178 print_error "FAIL: No membership found in database"
···1#!/usr/bin/env bash
2+# set -euo pipefail
34# ATB-15 Manual Testing Helper Script
5# Tests membership auto-creation during OAuth login
···48set +a
4950# Verify required variables
51+REQUIRED_VARS=("DATABASE_URL" "FORUM_DID" "PDS_URL" "OAUTH_PUBLIC_URL")
52for var in "${REQUIRED_VARS[@]}"; do
53 if [ -z "${!var:-}" ]; then
54 print_error "Required environment variable $var is not set in .env"
···86 exit 1
87fi
8889+# Ensure forum record exists in database
90+print_step "2" "Ensuring forum record exists"
91FORUM_URI="at://${FORUM_DID}/space.atbb.forum.forum/self"
9293+FORUM_COUNT=$(psql "$DATABASE_URL" -t -c \
94+ "SELECT COUNT(*) FROM forums WHERE did = '$FORUM_DID' AND rkey = 'self';" \
95+ 2>/dev/null | tr -d ' ')
96+97+if [ "$FORUM_COUNT" -eq 0 ]; then
98+ print_info "No forum record found. Creating one for testing..."
99+ psql "$DATABASE_URL" -c \
100+ "INSERT INTO forums (did, rkey, cid, name, description, indexed_at)
101+ VALUES ('$FORUM_DID', 'self', 'bafytest123', 'Test Forum', 'Test forum for membership creation', NOW());" \
102+ 2>/dev/null
103+ print_success "Created forum record in database"
104+else
105+ print_success "Forum record already exists"
106+fi
107+108+# Check if membership already exists in database
109+print_step "3" "Checking database for existing membership"
110+111MEMBERSHIP_COUNT=$(psql "$DATABASE_URL" -t -c \
112+ "SELECT COUNT(*) FROM memberships WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';" \
113 2>/dev/null | tr -d ' ')
114115if [ "$MEMBERSHIP_COUNT" -gt 0 ]; then
···118 echo
119 if [[ $REPLY =~ ^[Yy]$ ]]; then
120 psql "$DATABASE_URL" -c \
121+ "DELETE FROM memberships WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';"
122 print_success "Deleted existing membership from database"
123 fi
124else
···126fi
127128# Instructions for OAuth flow
129+print_step "4" "OAuth Login Flow"
130+echo ""
131+132+# Construct OAuth login URL
133+LOGIN_URL="${OAUTH_PUBLIC_URL}/api/auth/login?handle=${TEST_HANDLE}"
134+135+echo "Open this URL in your browser to start OAuth login:"
136+echo ""
137+echo -e "${GREEN}${LOGIN_URL}${NC}"
138+echo ""
139+echo "What will happen:"
140+echo " 1. Browser redirects to your PDS (${PDS_URL})"
141+echo " 2. You approve the forum's access request"
142+echo " 3. PDS redirects back to ${OAUTH_PUBLIC_URL}/api/auth/callback"
143+echo " 4. Forum creates session and membership record"
144+echo " 5. Browser redirects to homepage"
145echo ""
146+print_info "Tip: Copy the URL above or use 'open \"$LOGIN_URL\"' on macOS"
00000147echo ""
148read -p "Press Enter after completing OAuth login..."
149150# Check server logs for membership creation
151+print_step "5" "Checking server logs"
152+print_info "Check your dev server console for these log events..."
1530154echo ""
155+echo "Expected log events (JSON formatted):"
156+echo " • oauth.login.initiated - OAuth flow started"
157+echo " • oauth.callback.success - User authenticated successfully"
158+echo " • oauth.callback.membership.created - New membership record created (first login)"
159+echo " • oauth.callback.membership.exists - Membership already exists (repeated login)"
160+echo " • oauth.callback.membership.failed - Membership creation failed (error case)"
161echo ""
162+print_info "The membership creation happens during the callback, so look for the membership event"
163164# Query database for indexed membership
165+print_step "6" "Checking database for indexed membership"
166sleep 2 # Give firehose a moment to index
167168MEMBERSHIP_DATA=$(psql "$DATABASE_URL" -c \
169+ "SELECT did, rkey, forum_uri, joined_at, created_at, indexed_at
170 FROM memberships
171+ WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';" \
172 2>/dev/null)
173174if echo "$MEMBERSHIP_DATA" | grep -q "$TEST_DID"; then
···181fi
182183# Test repeated login
184+print_step "7" "Test repeated login (no duplicate)"
185+echo ""
186+187+# Construct logout and login URLs
188+LOGOUT_URL="${OAUTH_PUBLIC_URL}/api/auth/logout"
189+190+echo "First, logout to clear the current session:"
191+echo ""
192+echo -e "${GREEN}${LOGOUT_URL}${NC}"
193+echo ""
194+print_info "Tip: Open the URL or use 'open \"$LOGOUT_URL\"' on macOS"
195echo ""
196+read -p "Press Enter after logging out..."
197+echo ""
198+echo "Now login again with the same account:"
199+echo ""
200+echo -e "${GREEN}${LOGIN_URL}${NC}"
201+echo ""
202+echo "Expected behavior:"
203+echo " • Login should succeed normally"
204+echo " • Server logs should show \"Membership already exists\" (not \"created\")"
205+echo " • No duplicate membership record in database"
206echo ""
207+read -p "Press Enter after completing repeated login..."
208209# Verify no duplicate in database
210FINAL_COUNT=$(psql "$DATABASE_URL" -t -c \
211+ "SELECT COUNT(*) FROM memberships WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';" \
212 2>/dev/null | tr -d ' ')
213214if [ "$FINAL_COUNT" -eq 1 ]; then
···216elif [ "$FINAL_COUNT" -gt 1 ]; then
217 print_error "FAIL: Found $FINAL_COUNT memberships (duplicates created!)"
218 psql "$DATABASE_URL" -c \
219+ "SELECT * FROM memberships WHERE did = '$TEST_DID' AND forum_uri = '$FORUM_URI';"
220 exit 1
221else
222 print_error "FAIL: No membership found in database"