feat(docs): add collapsible sidebar and new documentation pages
Restructure sidebar into Tauri-style collapsible sections (Quick Start, Core Concepts, Guides, Reference) with auto-expand on active page. Add four new stub pages: What is Portal, Prerequisites, Security Model, and SIWE Authentication. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Yechan Kim committed
Apr 12, 2026 at 19:29 UTC
915fdf7ebb6c758d9e4e0e695137d59a5a8fa8e7
6 files changed
+312
-41
docs/src/lib/components/Sidebar.svelte
+92
-29
@@ -4,40 +4,103 @@
4
import { navigation } from '$lib/nav';
5
6
const currentPath = $derived($page.url.pathname);
7
+
8
+ // Track user-toggled state per section. null means "not manually toggled"
9
+ let userToggles: Record<number, boolean | null> = $state({});
10
+
11
+ function isItemActive(href: string): boolean {
12
+ return (
13
+ currentPath === `${base}${href}/` ||
14
+ currentPath === `${base}${href}` ||
15
+ currentPath.startsWith(`${base}${href}/`)
16
+ );
17
+ }
18
+
19
+ function sectionHasActiveChild(sectionIndex: number): boolean {
20
+ return navigation[sectionIndex].items.some((item) => isItemActive(item.href));
21
+ }
22
+
23
+ function isSectionOpen(index: number): boolean {
24
+ const userToggle = userToggles[index];
25
+ if (userToggle !== null && userToggle !== undefined) {
26
+ return userToggle;
27
+ }
28
+ // Auto-open if section has active child or defaultOpen
29
+ return sectionHasActiveChild(index) || (navigation[index].defaultOpen ?? false);
30
+ }
31
+
32
+ function toggleSection(index: number) {
33
+ const currentlyOpen = isSectionOpen(index);
34
+ userToggles[index] = !currentlyOpen;
35
+ }
36
+
37
+ // When the route changes to a page within a section, force that section open
38
+ $effect(() => {
39
+ // Access currentPath to create the dependency
40
+ void currentPath;
41
+ for (let i = 0; i < navigation.length; i++) {
42
+ if (sectionHasActiveChild(i)) {
43
+ userToggles[i] = true;
44
+ }
45
+ }
46
+ });
47
</script>
48
9
-<nav class="space-y-6" aria-label="Documentation">
10
- {#each navigation as section}
49
+<nav class="space-y-1" aria-label="Documentation">
50
+ {#each navigation as section, index (section.title)}
51
+ {@const isOpen = isSectionOpen(index)}
52
<div>
12
- <h3
13
- class="flex items-center gap-2 font-display text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400"
53
+ <button
54
+ type="button"
55
+ class="flex w-full cursor-pointer items-center justify-between rounded-md px-1 py-2 font-display text-xs font-semibold tracking-wider text-text-muted uppercase transition-colors hover:text-foreground"
56
+ aria-expanded={isOpen}
57
+ onclick={() => toggleSection(index)}
58
>
15
- {section.title}
16
- {#if section.badge}
17
- <span
18
- class="rounded-full border border-amber-400/25 bg-amber-400/15 px-1.5 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400"
19
- >
20
- {section.badge}
21
- </span>
22
- {/if}
23
- </h3>
24
- <ul class="mt-2 space-y-1">
25
- {#each section.items as item}
26
- {@const isActive =
27
- currentPath === `${base}${item.href}/` || currentPath === `${base}${item.href}` || currentPath.startsWith(`${base}${item.href}/`)}
28
- <li>
29
- <a
30
- href="{base}{item.href}"
31
- class="block rounded-lg px-3 py-2 text-sm transition-colors {isActive
32
- ? 'bg-primary/10 font-medium text-primary dark:bg-primary-light/15 dark:text-primary-light'
33
- : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/6'}"
34
- aria-current={isActive ? 'page' : undefined}
59
+ <span class="flex items-center gap-2">
60
+ {section.title}
61
+ {#if section.badge}
62
+ <span
63
+ class="rounded-full border border-amber-400/25 bg-amber-400/15 px-1.5 py-0.5 text-[10px] font-medium tracking-normal normal-case text-amber-600 dark:text-amber-400"
64
>
36
- {item.title}
37
- </a>
38
- </li>
39
- {/each}
40
- </ul>
65
+ {section.badge}
66
+ </span>
67
+ {/if}
68
+ </span>
69
+ <svg
70
+ class="h-3.5 w-3.5 shrink-0 transition-transform duration-200 {isOpen
71
+ ? 'rotate-90'
72
+ : 'rotate-0'}"
73
+ viewBox="0 0 16 16"
74
+ fill="currentColor"
75
+ aria-hidden="true"
76
+ >
77
+ <path
78
+ d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z"
79
+ />
80
+ </svg>
81
+ </button>
82
+ <div
83
+ class="grid transition-[grid-template-rows] duration-200 ease-in-out {isOpen
84
+ ? 'grid-rows-[1fr]'
85
+ : 'grid-rows-[0fr]'}"
86
+ >
87
+ <ul class="overflow-hidden">
88
+ {#each section.items as item (item.href)}
89
+ {@const isActive = isItemActive(item.href)}
90
+ <li>
91
+ <a
92
+ href="{base}{item.href}"
93
+ class="block rounded-lg px-3 py-2 text-sm transition-colors {isActive
94
+ ? 'bg-primary/10 font-medium text-primary dark:bg-primary-light/15 dark:text-primary-light'
95
+ : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/6'}"
96
+ aria-current={isActive ? 'page' : undefined}
97
+ >
98
+ {item.title}
99
+ </a>
100
+ </li>
101
+ {/each}
102
+ </ul>
103
+ </div>
104
</div>
105
{/each}
106
</nav>
docs/src/lib/nav.ts
+20
-12
@@ -6,6 +6,7 @@ export interface NavItem {
6
export interface NavSection {
7
title: string;
8
badge?: string;
9
+ defaultOpen?: boolean;
10
items: NavItem[];
11
}
12
@@ -35,30 +36,37 @@ export function getPrevNext(
36
37
export const navigation: NavSection[] = [
38
{
38
- title: 'Getting Started',
39
+ title: 'Quick Start',
40
+ defaultOpen: true,
41
items: [
40
- { title: 'Quick Start', href: '/getting-started' },
41
- { title: 'Concepts', href: '/concepts' },
42
- { title: 'Self-Hosting', href: '/self-hosting' }
42
+ { title: 'What is Portal?', href: '/what-is-portal' },
43
+ { title: 'Prerequisites', href: '/prerequisites' },
44
+ { title: 'Getting Started', href: '/getting-started' }
45
]
46
},
47
{
46
- title: 'Guides',
47
- items: [{ title: 'TCP/UDP Tunneling', href: '/tcp-udp-tunneling' }]
48
+ title: 'Core Concepts',
49
+ items: [
50
+ { title: 'Overview', href: '/concepts' },
51
+ { title: 'Architecture', href: '/architecture' },
52
+ { title: 'Security Model', href: '/security-model' }
53
+ ]
54
},
55
{
50
- title: 'Reference',
56
+ title: 'Guides',
57
items: [
52
- { title: 'CLI Reference', href: '/cli-reference' },
53
- { title: 'API Reference', href: '/api-reference' },
58
+ { title: 'Self-Hosting', href: '/self-hosting' },
59
+ { title: 'TCP/UDP Tunneling', href: '/tcp-udp-tunneling' },
60
+ { title: 'SIWE Authentication', href: '/siwe-authentication' },
61
+ { title: 'Deployment', href: '/deployment' },
62
{ title: 'Configuration', href: '/configuration' }
63
]
64
},
65
{
58
- title: 'Advanced',
66
+ title: 'Reference',
67
items: [
60
- { title: 'Architecture', href: '/architecture' },
61
- { title: 'Deployment', href: '/deployment' }
68
+ { title: 'CLI Reference', href: '/cli-reference' },
69
+ { title: 'API Reference', href: '/api-reference' }
70
]
71
}
72
];
docs/src/routes/prerequisites/+page.md
new
+41
@@ -0,0 +1,41 @@
1
+---
2
+title: Prerequisites
3
+description: System requirements and prerequisites for running Portal tunnel.
4
+---
5
+
6
+# Prerequisites
7
+
8
+Before installing Portal, make sure your environment meets the following requirements.
9
+
10
+## System Requirements
11
+
12
+| Requirement | Minimum |
13
+|-------------|---------|
14
+| OS | Linux (amd64/arm64), macOS (amd64/arm64), Windows (amd64) |
15
+| Network | Outbound TCP access (no inbound ports needed) |
16
+| Disk | ~10 MB for the binary |
17
+
18
+## For Tunnel Users
19
+
20
+- A local service running on a TCP port (e.g., a web server on `localhost:3000`)
21
+- Internet connectivity to reach a relay server
22
+
23
+No accounts, API keys, or billing setup required.
24
+
25
+## For Relay Operators
26
+
27
+If you plan to run your own relay server:
28
+
29
+- A server with a public IP address
30
+- A domain name with DNS pointing to the server
31
+- TLS certificate (auto-provisioned via ACME/Let's Encrypt, or manually provided)
32
+- Ports 443 (HTTPS) and optionally 80 (HTTP redirect) open
33
+
34
+## Optional
35
+
36
+- **ENS name** — for SIWE-based identity and portable naming
37
+- **Ethereum wallet** — for signing SIWE authentication messages
38
+
39
+## Next Steps
40
+
41
+- [Getting Started](/getting-started) — install Portal and create your first tunnel
docs/src/routes/security-model/+page.md
new
+60
@@ -0,0 +1,60 @@
1
+---
2
+title: Security Model
3
+description: How Portal ensures end-to-end encryption and prevents relay-level eavesdropping.
4
+---
5
+
6
+# Security Model
7
+
8
+Portal's security model is designed so that relay operators **cannot read tunnel traffic**, even though all data passes through their servers.
9
+
10
+## End-to-End TLS
11
+
12
+Tunnel traffic is encrypted with TLS between the client (browser) and your local service. The relay only sees opaque TCP bytes.
13
+
14
+```
15
+Client (browser) <-- TLS --> Your local app
16
+ | ^
17
+ | opaque TCP bytes |
18
+ v |
19
+ Relay server --- forwards ----->
20
+```
21
+
22
+The relay performs **TCP passthrough** — it connects raw TCP streams without terminating TLS.
23
+
24
+## MITM Detection
25
+
26
+Portal includes built-in MITM detection:
27
+
28
+1. The tunnel client generates a TLS certificate locally
29
+2. The certificate fingerprint is embedded in the public URL
30
+3. Connecting clients verify the fingerprint matches the server certificate
31
+4. Any relay-level interception would present a different certificate, triggering a mismatch
32
+
33
+## Relay Trust Model
34
+
35
+| What relays CAN see | What relays CANNOT see |
36
+|---------------------|----------------------|
37
+| Connection metadata (IP, timing) | Request/response content |
38
+| Tunnel name and domain | HTTP headers or body |
39
+| Traffic volume (bytes) | TLS-encrypted payload |
40
+| Connection duration | Application-layer data |
41
+
42
+## SIWE Authentication
43
+
44
+Portal supports Sign-In with Ethereum (SIWE) for identity:
45
+
46
+- Proves ownership of a tunnel name without a centralized auth server
47
+- ENS names provide portable, human-readable identity
48
+- No passwords or API keys stored anywhere
49
+
50
+## Best Practices
51
+
52
+1. **Always use HTTPS** — Portal provisions TLS certificates automatically
53
+2. **Verify certificate fingerprints** for sensitive applications
54
+3. **Run your own relay** if you need full control over the infrastructure
55
+4. **Rotate tunnel names** for temporary or throwaway use cases
56
+
57
+## Next Steps
58
+
59
+- [Architecture](/architecture) — deep dive into Portal's internal design
60
+- [Self-Hosting](/self-hosting) — run your own relay server
docs/src/routes/siwe-authentication/+page.md
new
+63
@@ -0,0 +1,63 @@
1
+---
2
+title: SIWE Authentication
3
+description: Use Sign-In with Ethereum (SIWE) and ENS for portable tunnel identity.
4
+---
5
+
6
+# SIWE Authentication
7
+
8
+Portal supports **Sign-In with Ethereum (SIWE)** for proving ownership of tunnel names without centralized accounts or API keys.
9
+
10
+## Overview
11
+
12
+SIWE allows you to:
13
+
14
+- **Claim a tunnel name** by signing a message with your Ethereum wallet
15
+- **Prove ownership** without passwords, tokens, or a central auth server
16
+- **Use ENS names** for human-readable, portable identity (e.g., `alice.eth`)
17
+
18
+## How It Works
19
+
20
+1. You choose a tunnel name (or use your ENS name)
21
+2. Portal generates a SIWE message containing the tunnel name and relay domain
22
+3. You sign the message with your Ethereum wallet (e.g., MetaMask, hardware wallet)
23
+4. The signed message is sent to the relay server
24
+5. The relay verifies the signature on-chain and grants the tunnel name
25
+
26
+```
27
+Wallet --> Sign SIWE message --> Portal CLI --> Relay server
28
+ |
29
+ Verify signature
30
+ |
31
+ Grant tunnel name
32
+```
33
+
34
+## ENS Integration
35
+
36
+If you own an ENS name, you can use it directly as your tunnel name:
37
+
38
+- `alice.eth` becomes your portable identity across relays
39
+- No registration or DNS configuration needed
40
+- Works with any relay in the public registry
41
+
42
+## Configuration
43
+
44
+```bash
45
+# Use SIWE authentication with a specific tunnel name
46
+portal-tunnel --auth siwe --name my-tunnel localhost:3000
47
+
48
+# Use your ENS name
49
+portal-tunnel --auth siwe --name alice.eth localhost:3000
50
+```
51
+
52
+## Without SIWE
53
+
54
+SIWE is optional. Without it:
55
+
56
+- Tunnel names are assigned on a first-come, first-served basis
57
+- No ownership guarantee — anyone can claim an unused name
58
+- Suitable for temporary or throwaway tunnels
59
+
60
+## Next Steps
61
+
62
+- [Security Model](/security-model) — understand Portal's encryption and trust model
63
+- [Configuration](/configuration) — full configuration reference
docs/src/routes/what-is-portal/+page.md
new
+36
@@ -0,0 +1,36 @@
1
+---
2
+title: What is Portal?
3
+description: An introduction to Portal — a permissionless localhost tunnel and public relay system.
4
+---
5
+
6
+# What is Portal?
7
+
8
+Portal is an open-source localhost tunnel that publishes local services to the public internet through relay servers — without login, billing, or cloud SaaS dependencies.
9
+
10
+## Key Properties
11
+
12
+- **Permissionless** — no account or API key required. Run the command and you're live.
13
+- **End-to-end TLS** — tenant TLS terminates locally with MITM detection; relays never see plaintext.
14
+- **Self-hostable** — use the public relay registry, discovered relay pools with failover, or run your own relay.
15
+- **Raw TCP** — carries HTTP, gRPC, WebSocket, and arbitrary TCP protocols without SSH or WebSocket overlays.
16
+
17
+## How It Works
18
+
19
+1. You start a local app (e.g., `localhost:3000`).
20
+2. You run `portal-tunnel` which connects to a relay server.
21
+3. The relay assigns a public HTTPS URL (e.g., `your-name.relay.example.com`).
22
+4. Incoming traffic is forwarded through the relay to your local app via an encrypted tunnel.
23
+
24
+## When to Use Portal
25
+
26
+| Use Case | Example |
27
+|----------|---------|
28
+| Share a dev server | Show a colleague your local branch |
29
+| Webhook development | Receive Stripe/GitHub webhooks locally |
30
+| Demo to clients | Temporary public URL for a staging app |
31
+| IoT / edge devices | Expose a device behind NAT |
32
+
33
+## Next Steps
34
+
35
+- [Prerequisites](/prerequisites) — what you need before installing
36
+- [Getting Started](/getting-started) — install and run your first tunnel