Implemented public galaxy viewing feature that allows unauthenticated
users to view public thought galaxies via the ?user={did} parameter,
while maintaining privacy controls for node-level visibility.
Changes:
- Updated /api/galaxy/route.ts to support public access:
* Accept ?user={did} query parameter for viewing specific user's galaxy
* Show all nodes (including private) for authenticated user viewing own galaxy
* Filter to only public nodes when viewing someone else's galaxy
* Return empty state with helpful message when not authenticated
* Filter links to only show connections between visible nodes
- Added is_public field to database schema:
* Updated db/schema.surql with DEFAULT true (public by default)
* Created migration script scripts/add-is-public-field.ts
* Aligns with ATproto's public-by-default philosophy
- Enhanced ThoughtGalaxy component:
* Support viewing galaxies via ?user={did} parameter
* Display user info banner when viewing public galaxy
* Show appropriate empty state messages based on context
* Refetch data when user parameter changes
- Created comprehensive Magnitude tests:
* Test public galaxy viewing without authentication
* Verify private nodes are hidden from public view
* Test own galaxy access requires authentication
* Validate invalid user DID handling
* Test user info display and navigation between galaxies
- Documented implementation plan in plans/10-public-galaxy-viewing.md
This implements the "public by default" model while allowing future
node-level privacy controls. All canonical data remains on the user's
ATproto PDS, with SurrealDB serving as a high-performance cache.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
8.9 KiB
Plan: Make Galaxy Viewable Without Login Requirement
Status
- Priority: HIGH (User-requested)
- Status: In Progress
- Created: 2025-01-10
Problem Statement
Currently, the galaxy visualization (/galaxy) requires user authentication via JWT cookie. This prevents:
- Public sharing of thought galaxies
- First-time visitors from seeing example galaxies
- Social media link previews from working properly
- Search engines from indexing public thought networks
The galaxy should be publicly viewable while still respecting user privacy preferences.
Current Implementation Analysis
Authentication Check
app/api/galaxy/route.ts (lines 27-38):
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
if (!surrealJwt) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const userSession = verifySurrealJwt(surrealJwt);
if (!userSession) {
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
}
Data Query
Currently queries WHERE user_did = $userDid (line 49), showing only the authenticated user's nodes.
Design Decisions
Option 1: Public by Default (RECOMMENDED)
All galaxies are publicly viewable, but users can mark individual nodes as private.
Pros:
- Simple implementation
- Encourages public knowledge sharing
- Better for SEO and discovery
- Aligns with "decentralized social" vision
Cons:
- Users might accidentally share private thoughts
- Requires clear UI indicators for node visibility
URL Structure:
/galaxy- current user's galaxy (if logged in) or landing page/galaxy/{user_did}or/galaxy?user={user_did}- specific user's public galaxy
Option 2: Opt-in Public Galleries
Galaxies are private by default, users must explicitly make them public.
Pros:
- More privacy-conscious
- Users have full control
Cons:
- Reduces discovery and sharing
- More complex implementation
- Goes against ATproto's "public by default" philosophy
Decision: We'll implement Option 1 - Public by default, with optional private nodes.
Implementation Plan
Phase 1: Update API to Support Public Access
1.1 Modify /api/galaxy/route.ts
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const targetUserDid = searchParams.get('user');
const cookieStore = await cookies();
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
// Determine which user's galaxy to show
let userDid: string;
let isOwnGalaxy = false;
if (targetUserDid) {
// Viewing someone else's public galaxy
userDid = targetUserDid;
} else if (surrealJwt) {
// Viewing own galaxy (authenticated)
const userSession = verifySurrealJwt(surrealJwt);
if (!userSession) {
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
}
userDid = userSession.did;
isOwnGalaxy = true;
} else {
// No target user and not authenticated - return empty galaxy with message
return NextResponse.json({
nodes: [],
links: [],
message: 'Log in to view your galaxy, or visit a public galaxy via ?user={did}'
});
}
// Query nodes
const nodesQuery = `
SELECT id, title, body, user_did, atp_uri, coords_3d
FROM node
WHERE user_did = $userDid
AND coords_3d != NONE
${isOwnGalaxy ? '' : 'AND is_public = true'}
`;
// ... rest of implementation
}
1.2 Add is_public Field to Node Schema
- Default:
true(public by default) - Users can mark individual nodes as private
- Private nodes are only visible to the owner
Phase 2: Update Database Schema
2.1 Add is_public Column to node Table
DEFINE FIELD is_public ON TABLE node TYPE bool DEFAULT true;
2.2 Create Migration Script
scripts/add-is-public-field.ts:
import { connectToDB } from '@/lib/db';
async function migrate() {
const db = await connectToDB();
// Add field definition
await db.query(`
DEFINE FIELD is_public ON TABLE node TYPE bool DEFAULT true;
`);
// Set existing nodes to public
await db.query(`
UPDATE node SET is_public = true WHERE is_public = NONE;
`);
console.log('Migration complete: Added is_public field');
}
migrate();
Phase 3: Update Frontend Components
3.1 Update ThoughtGalaxy.tsx
// Support viewing other users' galaxies
const { searchParams } = useSearchParams();
const targetUser = searchParams.get('user');
useEffect(() => {
async function fetchData() {
const url = targetUser
? `/api/galaxy?user=${targetUser}`
: '/api/galaxy';
const response = await fetch(url, {
credentials: 'include',
});
// ... rest of implementation
}
fetchData();
}, [targetUser]);
3.2 Add User Info Display
When viewing another user's galaxy, show:
- User's Bluesky handle
- Link to their profile
- Number of public nodes
3.3 Update /galaxy Page
Add support for URL parameter: /galaxy?user=did:plc:xxxxx
Phase 4: Navigation & User Experience
4.1 Landing Experience for Non-Authenticated Users
When visiting /galaxy without login:
- Show a sample/demo galaxy (could be a curated example)
- Display call-to-action: "Create your own thought galaxy"
- Provide login button
4.2 Add "Share Galaxy" Feature
Add button to copy shareable link:
const shareUrl = `${window.location.origin}/galaxy?user=${userDid}`;
Phase 5: Privacy Controls (Future Enhancement)
5.1 Node-Level Privacy Toggle
In node editor, add checkbox:
<Checkbox
label="Make this node public"
checked={isPublic}
onChange={(e) => setIsPublic(e.currentTarget.checked)}
/>
5.2 Bulk Privacy Management
Settings page to:
- Make all nodes private/public
- Set default for new nodes
- Filter and update specific nodes
Security Considerations
1. Data Exposure
- Risk: Users accidentally share sensitive information
- Mitigation:
- Clear visual indicators for public/private nodes
- Confirmation dialog when publishing nodes
- Easy way to make nodes private retroactively
2. API Abuse
- Risk: Scraping or excessive requests to public galaxies
- Mitigation:
- Rate limiting on
/api/galaxy - Caching layer for public galaxies
- Consider CDN for popular galaxies
- Rate limiting on
3. Privacy Violations
- Risk: Viewing history tracking or surveillance
- Mitigation:
- No analytics on public galaxy views
- No "who viewed my galaxy" feature
- Respect DNT headers
Testing Plan
Magnitude Tests
Test 1: Public Galaxy Viewing Without Auth
test('Unauthenticated users can view public galaxies', async (agent) => {
await agent.act('Navigate to /galaxy?user=did:plc:example123');
await agent.check('The galaxy visualization is displayed');
await agent.check('Public nodes are visible');
await agent.act('Click on a public node');
await agent.check('Node details are displayed');
});
Test 2: Private Nodes Hidden from Public View
test('Private nodes are not visible in public galaxy', async (agent) => {
// ... implementation
});
Test 3: Own Galaxy Requires Auth
test('Accessing own galaxy without target user requires authentication', async (agent) => {
await agent.act('Navigate to /galaxy');
await agent.check('Login prompt or empty state is displayed');
});
Manual Testing Checklist
- Visit
/galaxywithout login → see landing page - Visit
/galaxy?user={valid_did}→ see public nodes - Visit
/galaxy?user={invalid_did}→ see error message - Log in and visit
/galaxy→ see own galaxy (including private nodes) - Share galaxy link → recipient can view public nodes
- Mark node as private → confirm it disappears from public view
Implementation Steps
- Create database migration for
is_publicfield - Update API route to support public access
- Update ThoughtGalaxy component to handle URL parameters
- Add user info display for public galaxies
- Test with manual checks
- Write Magnitude tests
- Update documentation
- Create PR with changes
Acceptance Criteria
✅ Unauthenticated users can view public galaxies via ?user= parameter
✅ Authenticated users see their own galaxy at /galaxy (no param)
✅ Private nodes are only visible to the owner
✅ Public nodes are visible to everyone
✅ Clear error messages for invalid user DIDs
✅ Shareable URLs work correctly
✅ All tests pass
Notes
- This aligns with ATproto's philosophy of public-by-default, user-controlled data
- Future enhancement: Node-level privacy controls in UI
- Consider adding Open Graph meta tags for social media previews
- May want to add a "featured galaxies" page for discovery
Related Files
app/api/galaxy/route.ts- Galaxy API endpointcomponents/ThoughtGalaxy.tsx- 3D visualization componentapp/galaxy/page.tsx- Galaxy page componentlib/db/schema.surql- Database schema