11 KiB
Architecture FAQ
Answers to common questions about gitrepublic-web's architecture and design decisions.
1. Session State
Where does session state live?
Answer: Session state lives entirely on the client (browser). There is no server-side session storage.
- Client-side: User's public key (
userPubkey) is stored in Svelte component state ($state) - No server storage: The server does not maintain session cookies, tokens, or any session database
- Stateless authentication: Each request is authenticated independently using:
- NIP-07: Browser extension (Alby, nos2x) for web UI operations
- NIP-98: HTTP authentication events for git operations
Implementation Details
// Client-side state (src/routes/+page.svelte)
let userPubkey = $state<string | null>(null);
// Login: Get pubkey from NIP-07 extension
async function login() {
userPubkey = await getPublicKeyWithNIP07();
}
// Logout: Clear client state
function logout() {
userPubkey = null;
}
Why stateless?
- Decentralized design: No central session authority
- Scalability: No session database to manage
- Privacy: Server doesn't track user sessions
- Nostr-native: Uses Nostr's cryptographic authentication
2. Session Scope
When does a session begin and end?
Answer: Since there's no server-side session, the "session" is really just client-side authentication state:
-
Begins: When user connects their NIP-07 extension and calls
login()- The extension provides the user's public key
- This is stored in component state for the current page load
-
Ends:
- When user calls
logout()(setsuserPubkey = null) - When browser tab/window is closed (state is lost)
- When page is refreshed (state is lost unless persisted)
- When user calls
Note: There's currently no persistence of login state across page refreshes. Users need to reconnect their NIP-07 extension on each page load.
Potential Enhancement: Could add localStorage to persist userPubkey across sessions, but this is a design decision - some prefer explicit re-authentication for security.
3. Repository Settings Storage
Where are repo settings stored?
Answer: Repository settings are stored entirely in Nostr events (kind 30617, NIP-34 repo announcements). No database is required.
Storage Location
- Nostr Events: All settings are stored as tags in the repository announcement event:
name: Repository namedescription: Repository descriptionclone: Clone URLs (array)maintainers: List of maintainer pubkeysprivate: Privacy flag (true/false)relays: Nostr relays to publish to
How It Works
-
Reading Settings:
// Fetch from Nostr relays const events = await nostrClient.fetchEvents([{ kinds: [KIND.REPO_ANNOUNCEMENT], authors: [ownerPubkey], '#d': [repoName], limit: 1 }]); // Extract settings from event tags const name = event.tags.find(t => t[0] === 'name')?.[1]; const maintainers = event.tags.filter(t => t[0] === 'maintainers').map(t => t[1]); -
Updating Settings:
// Create new announcement event with updated tags const updatedEvent = { kind: KIND.REPO_ANNOUNCEMENT, pubkey: ownerPubkey, tags: [ ['d', repoName], ['name', newName], ['maintainers', maintainer1], ['maintainers', maintainer2], ['private', 'true'] ] }; // Sign with NIP-07 and publish to relays const signed = await signEventWithNIP07(updatedEvent); await nostrClient.publishEvent(signed, relays);
Benefits
- Decentralized: Settings live on Nostr relays, not a central database
- Verifiable: Cryptographically signed by repository owner
- Resilient: Multiple relays store copies
- No database needed: Simplifies deployment
Limitations
- Event replaceability: NIP-34 announcements are replaceable (same
dtag), so latest event wins - Relay dependency: Settings are only as available as the relays
- No complex queries: Can't do complex database-style queries
4. NIP-98 Authorization Requirements
What actions require NIP-98 authorization?
Answer: NIP-98 is required for git operations (clone, push, pull) and optional for web UI file operations.
Required NIP-98 Operations
-
Git Push Operations (
POST /api/git/{npub}/{repo}.git/git-receive-pack)- Always required for push operations
- Verifies user is repository owner or maintainer
- Validates event signature, timestamp, URL, and method
-
Private Repository Clone/Fetch (
GET /api/git/{npub}/{repo}.git/info/refs?service=git-upload-pack)- Required if repository is marked as private
- Verifies user has view access (owner or maintainer)
- Public repos don't require authentication
-
Private Repository Fetch (
POST /api/git/{npub}/{repo}.git/git-upload-pack)- Required if repository is private
- Same authentication as clone
Optional NIP-98 Operations
- File Write Operations (
POST /api/repos/{npub}/{repo}/file)- Optional: Can use NIP-07 (browser extension) or NIP-98
- NIP-98 is useful for automated scripts or git operations
- NIP-07 is more convenient for web UI
NIP-98 Verification Process
// Server verifies:
1. Event signature (cryptographic verification)
2. Event timestamp (within 60 seconds)
3. URL matches request URL exactly
4. HTTP method matches
5. Payload hash matches request body (for POST)
6. Pubkey is repository owner or maintainer
API Endpoints Summary
| Endpoint | NIP-98 Required? | Notes |
|---|---|---|
GET /api/git/.../info/refs |
Only for private repos | Public repos: no auth needed |
POST /api/git/.../git-upload-pack |
Only for private repos | Public repos: no auth needed |
POST /api/git/.../git-receive-pack |
Always required | All push operations |
POST /api/repos/.../file |
Optional | Can use NIP-07 instead |
GET /api/repos/.../file |
No | Uses query param userPubkey |
POST /api/repos/.../settings |
No | Uses NIP-07 (browser extension) |
5. Repository Announcement Polling
Why is the server polling instead of using subscriptions?
Answer: The server uses polling (every 60 seconds) instead of persistent WebSocket subscriptions for simplicity and reliability.
Current Implementation
// src/lib/services/nostr/repo-polling.ts
constructor(
pollingInterval: number = 60000 // 1 minute default
) {
// Poll immediately, then every interval
this.intervalId = setInterval(() => {
this.poll();
}, this.pollingInterval);
}
Polling Schedule:
- Frequency: Every 60 seconds (1 minute)
- Type: Long-running background process
- Location: Started in
hooks.server.tswhen server starts - Not a cron job: Runs continuously in the Node.js process
Why Polling Instead of Subscriptions?
Advantages of Polling:
- Simplicity: No need to maintain persistent WebSocket connections
- Reliability: If a connection drops, polling automatically retries
- Resource efficiency: Only connects when fetching, not maintaining long-lived connections
- Easier error handling: Each poll is independent
Disadvantages of Polling:
- Latency: Up to 60 seconds delay before new repos are discovered
- Relay load: More frequent queries to relays
- Less real-time: Not immediate notification of new repos
Could We Use Subscriptions?
Yes, but with trade-offs:
// Potential subscription implementation
const ws = new WebSocket(relay);
ws.send(JSON.stringify(['REQ', 'sub-id', {
kinds: [KIND.REPO_ANNOUNCEMENT],
'#clone': [domain]
}]));
ws.on('message', (event) => {
// Handle new repo announcement immediately
});
Challenges:
- Need to maintain WebSocket connections to multiple relays
- Handle connection drops and reconnections
- More complex error handling
- Higher memory usage for long-lived connections
Recommendation
For most use cases, 60-second polling is acceptable:
- New repos don't need to be discovered instantly
- Reduces complexity
- More reliable for production
For real-time requirements, subscriptions could be added as an enhancement, but polling is a solid default.
6. Branch Protection
What is the scheme for branch protection?
Answer: Branch protection is not currently implemented. This is a missing feature.
Current State
What Exists:
- Maintainers can create branches (
POST /api/repos/{npub}/{repo}/branches) - Only maintainers can create branches (not regular users)
- No protection for
main/masterbranch
What's Missing:
- ❌ No branch protection rules
- ❌ No restriction on pushing to
main/master - ❌ No required pull request reviews
- ❌ No required status checks
- ❌ No force push restrictions
Current Authorization
// src/routes/api/repos/[npub]/[repo]/branches/+server.ts
const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, repoOwnerPubkey, repo);
if (!isMaintainer) {
return error(403, 'Only repository maintainers can create branches');
}
Authorized Users:
- Repository Owner: Can do everything
- Maintainers: Listed in repo announcement
maintainerstags- Can create branches
- Can push to any branch (including main)
- Can write files
Proposed Branch Protection Implementation
Option 1: Nostr Events (Recommended)
- Create new event kind (e.g., 30620) for branch protection rules
- Store rules in Nostr events:
{ "kind": 30620, "tags": [ ["d", "repo-name"], ["branch", "main", "protected"], ["branch", "main", "require-pr"], ["branch", "main", "require-reviewers", "pubkey1", "pubkey2"] ] }
Option 2: In-Repo Configuration
- Store
.gitrepublic/branch-protection.jsonin repository - Git-based, version-controlled
- Requires pull request to change rules
Option 3: Server Configuration
- Store in server database (conflicts with decentralized design)
- Not recommended for this architecture
Recommended Approach
Hybrid: Nostr Events + In-Repo Config
- Default rules: Stored in Nostr events (kind 30620)
- Override rules: Can be stored in
.gitrepublic/branch-protection.jsonin repo - Enforcement: Server checks rules before allowing push to protected branches
Example Rules:
{
"protectedBranches": ["main", "master"],
"requirePullRequest": true,
"requireReviewers": ["pubkey1", "pubkey2"],
"allowForcePush": false,
"requireStatusChecks": ["ci", "lint"]
}
Implementation Priority
This is a medium-priority feature that would enhance security and workflow, but the current system works for basic use cases where:
- Owners trust their maintainers
- Repositories are small teams
- Formal review processes aren't needed
For enterprise use cases, branch protection would be highly recommended.
Summary
| Question | Answer |
|---|---|
| Session State | Client-side only, no server storage |
| Session Scope | Begins on NIP-07 login, ends on logout or page close |
| Repo Settings | Stored in Nostr events (kind 30617), no database needed |
| NIP-98 Required | Git push (always), private repo clone/fetch (conditional) |
| Polling Schedule | Every 60 seconds, long-running background process |
| Branch Protection | ✅ Implemented - Stored in Nostr events (kind 30620) |