A community based topic aggregation platform built on atproto

feat(web): add privacy policy page for Play Store

Add /privacy route with privacy policy template covering:
- atProto data model and federation
- Data collection practices (minimal)
- 18+ age requirement
- Cloudflare as third-party service
- Contact information

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+331
+3
internal/api/routes/web.go
··· 30 30 r.Post("/delete-account", handlers.DeleteAccountSubmitHandler) 31 31 r.Get("/delete-account/success", handlers.DeleteAccountSuccessHandler) 32 32 33 + // Legal pages 34 + r.Get("/privacy", handlers.PrivacyHandler) 35 + 33 36 // Static files (images, etc.) 34 37 r.Get("/static/*", func(w http.ResponseWriter, r *http.Request) { 35 38 // Serve from project's static directory
+8
internal/web/handlers.go
··· 173 173 MaxAge: -1, 174 174 }) 175 175 } 176 + 177 + // PrivacyHandler handles GET /privacy requests and renders the privacy policy page. 178 + func (h *Handlers) PrivacyHandler(w http.ResponseWriter, r *http.Request) { 179 + if err := h.templates.Render(w, "privacy.html", nil); err != nil { 180 + slog.Error("failed to render privacy policy template", "error", err) 181 + http.Error(w, "Internal server error", http.StatusInternalServerError) 182 + } 183 + }
+290
internal/web/templates/privacy.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Privacy Policy - Coves</title> 7 + <link rel="icon" type="image/png" href="/static/images/lil_dude.png"> 8 + <style> 9 + * { box-sizing: border-box; margin: 0; padding: 0; } 10 + body { 11 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 12 + background: #0B0F14; 13 + color: #e4e6e7; 14 + line-height: 1.7; 15 + padding: 24px; 16 + } 17 + .container { 18 + max-width: 800px; 19 + margin: 0 auto; 20 + } 21 + h1 { 22 + font-size: 2rem; 23 + margin-bottom: 0.5rem; 24 + color: #e4e6e7; 25 + } 26 + h2 { 27 + font-size: 1.4rem; 28 + margin-top: 2rem; 29 + margin-bottom: 1rem; 30 + color: #e4e6e7; 31 + border-bottom: 1px solid #2A2F36; 32 + padding-bottom: 0.5rem; 33 + } 34 + h3 { 35 + font-size: 1.1rem; 36 + margin-top: 1.5rem; 37 + margin-bottom: 0.5rem; 38 + color: #e4e6e7; 39 + } 40 + p { 41 + margin-bottom: 1rem; 42 + color: #B6C2D2; 43 + } 44 + ul { 45 + margin-bottom: 1rem; 46 + padding-left: 1.5rem; 47 + } 48 + li { 49 + margin-bottom: 0.5rem; 50 + color: #B6C2D2; 51 + } 52 + a { 53 + color: #7CB9E8; 54 + text-decoration: none; 55 + } 56 + a:hover { 57 + text-decoration: underline; 58 + } 59 + .effective-date { 60 + color: #5A6B7F; 61 + font-size: 0.9rem; 62 + margin-bottom: 2rem; 63 + } 64 + .highlight { 65 + background: rgba(124, 185, 232, 0.1); 66 + border-left: 3px solid #7CB9E8; 67 + padding: 1rem; 68 + margin: 1rem 0; 69 + border-radius: 0 8px 8px 0; 70 + } 71 + .highlight p { 72 + margin-bottom: 0; 73 + } 74 + table { 75 + width: 100%; 76 + border-collapse: collapse; 77 + margin: 1rem 0; 78 + } 79 + th, td { 80 + text-align: left; 81 + padding: 0.75rem; 82 + border-bottom: 1px solid #2A2F36; 83 + } 84 + th { 85 + color: #e4e6e7; 86 + font-weight: 600; 87 + } 88 + td { 89 + color: #B6C2D2; 90 + } 91 + .back-link { 92 + display: inline-block; 93 + margin-bottom: 1.5rem; 94 + color: #5A6B7F; 95 + font-size: 0.9rem; 96 + } 97 + .back-link:hover { 98 + color: #7CB9E8; 99 + } 100 + @media (max-width: 600px) { 101 + body { 102 + padding: 1rem; 103 + } 104 + h1 { 105 + font-size: 1.5rem; 106 + } 107 + } 108 + </style> 109 + </head> 110 + <body> 111 + <div class="container"> 112 + <a href="/" class="back-link">&larr; Back to Coves</a> 113 + 114 + <h1>Privacy Policy</h1> 115 + <p class="effective-date">Effective Date: January 16, 2026</p> 116 + 117 + <p>Coves Team ("we," "our," or "us") operates the Coves mobile application and website. This Privacy Policy explains how we collect, use, and protect your information when you use our service.</p> 118 + 119 + <div class="highlight"> 120 + <p><strong>Summary:</strong> Coves is built on the atProto protocol. Your data is stored on your Personal Data Server (PDS), which you control. We only index and cache publicly available data from the network to provide our service.</p> 121 + </div> 122 + 123 + <h2>1. Information We Collect</h2> 124 + 125 + <h3>1.1 Account Information</h3> 126 + <p>When you sign in to Coves, we receive the following from your atProto identity:</p> 127 + <ul> 128 + <li><strong>Decentralized Identifier (DID):</strong> Your unique identifier on the atProto network</li> 129 + <li><strong>Handle:</strong> Your username (e.g., yourname.bsky.social)</li> 130 + <li><strong>Profile Information:</strong> Display name, bio, and avatar that you've set on your PDS</li> 131 + </ul> 132 + 133 + <h3>1.2 Content You Create</h3> 134 + <p>When you use Coves, the content you create is written to your PDS:</p> 135 + <ul> 136 + <li>Posts and comments</li> 137 + <li>Votes (likes and downvotes)</li> 138 + <li>Community memberships</li> 139 + </ul> 140 + 141 + <h3>1.3 Information We Do NOT Collect</h3> 142 + <table> 143 + <tr> 144 + <th>Data Type</th> 145 + <th>Collected?</th> 146 + </tr> 147 + <tr> 148 + <td>Passwords</td> 149 + <td>No - OAuth only</td> 150 + </tr> 151 + <tr> 152 + <td>Device identifiers (IMEI, serial numbers)</td> 153 + <td>No</td> 154 + </tr> 155 + <tr> 156 + <td>Location data</td> 157 + <td>No</td> 158 + </tr> 159 + <tr> 160 + <td>Contacts</td> 161 + <td>No</td> 162 + </tr> 163 + <tr> 164 + <td>Photos/media from your device</td> 165 + <td>No</td> 166 + </tr> 167 + <tr> 168 + <td>Analytics or telemetry</td> 169 + <td>No</td> 170 + </tr> 171 + <tr> 172 + <td>Advertising identifiers</td> 173 + <td>No</td> 174 + </tr> 175 + <tr> 176 + <td>Crash reports</td> 177 + <td>No</td> 178 + </tr> 179 + </table> 180 + 181 + <h2>2. How We Use Your Information</h2> 182 + <p>We use the information we collect to:</p> 183 + <ul> 184 + <li>Authenticate you and maintain your session</li> 185 + <li>Display your profile and content within the app</li> 186 + <li>Show you posts, comments, and communities</li> 187 + <li>Process your votes and interactions</li> 188 + </ul> 189 + <p>We do not use your information for advertising, profiling, or selling to third parties.</p> 190 + 191 + <h2>3. The atProto Protocol and Federation</h2> 192 + 193 + <div class="highlight"> 194 + <p><strong>Important:</strong> Coves is built on the atProto (AT Protocol), a federated social networking protocol. This affects how your data works.</p> 195 + </div> 196 + 197 + <h3>3.1 Your Personal Data Server (PDS)</h3> 198 + <p>Your content (posts, comments, votes) is stored on your PDS, not on Coves servers. You may host your own PDS or use a hosted provider. You maintain control over your data at the PDS level.</p> 199 + 200 + <h3>3.2 What Coves Stores</h3> 201 + <p>Coves operates an AppView that indexes publicly available data from the atProto network firehose. This means:</p> 202 + <ul> 203 + <li>We cache and index your public content to provide fast access</li> 204 + <li>Your authentication tokens are stored encrypted on your device</li> 205 + <li>We do not store copies of your private data</li> 206 + </ul> 207 + 208 + <h3>3.3 Federation</h3> 209 + <p>Because atProto is federated, your public content may be visible on other applications and services that use the protocol. This is a feature of the protocol, not something Coves controls. When you post content, consider that it may be indexed and displayed by other atProto services.</p> 210 + 211 + <h2>4. Data Storage and Security</h2> 212 + 213 + <h3>4.1 Where Data is Stored</h3> 214 + <ul> 215 + <li><strong>Your device:</strong> Authentication tokens are stored in encrypted storage (iOS Keychain / Android EncryptedSharedPreferences)</li> 216 + <li><strong>Your PDS:</strong> Your content is stored on your Personal Data Server</li> 217 + <li><strong>Our servers:</strong> Located in Canada, used for indexing public atProto data</li> 218 + </ul> 219 + 220 + <h3>4.2 Security Measures</h3> 221 + <ul> 222 + <li>OAuth 2.0 with DPoP for authentication</li> 223 + <li>Encrypted token storage on device</li> 224 + <li>HTTPS for all network communications</li> 225 + <li>No plaintext storage of sensitive data</li> 226 + </ul> 227 + 228 + <h2>5. Third-Party Services</h2> 229 + <p>We use the following third-party services:</p> 230 + <ul> 231 + <li><strong>Cloudflare:</strong> For content delivery, DDoS protection, and DNS. Cloudflare may process your IP address and request metadata. See <a href="https://www.cloudflare.com/privacy/" target="_blank" rel="noopener">Cloudflare's Privacy Policy</a>.</li> 232 + </ul> 233 + <p>We do not use third-party analytics, advertising networks, or tracking services.</p> 234 + 235 + <h2>6. Data Retention and Deletion</h2> 236 + 237 + <h3>6.1 On Coves Servers</h3> 238 + <p>When you delete content or your account:</p> 239 + <ul> 240 + <li>Your data is removed from our index immediately</li> 241 + <li>We do not retain backups of deleted content</li> 242 + <li>Cached data is purged from our systems</li> 243 + </ul> 244 + 245 + <h3>6.2 On Your PDS</h3> 246 + <p>Content stored on your PDS is managed by you or your PDS provider. Deleting content from Coves removes it from our index, but the original data on your PDS must be managed separately according to your PDS provider's policies.</p> 247 + 248 + <h3>6.3 Federation Caveat</h3> 249 + <p>Due to the federated nature of atProto, content that was public may have been indexed or cached by other services before deletion. We cannot control data held by other parties on the network.</p> 250 + 251 + <h2>7. Your Rights</h2> 252 + <p>You have the right to:</p> 253 + <ul> 254 + <li><strong>Access:</strong> View all data associated with your account</li> 255 + <li><strong>Delete:</strong> Remove your content from our index by deleting it in the app or signing out</li> 256 + <li><strong>Control:</strong> Manage your data directly on your PDS</li> 257 + <li><strong>Portability:</strong> Your data lives on your PDS and can be moved to another atProto service</li> 258 + </ul> 259 + <p>To exercise these rights, contact us at <a href="mailto:support@coves.social">support@coves.social</a>.</p> 260 + 261 + <h2>8. Age Requirement</h2> 262 + <p>Coves is intended for users who are <strong>18 years of age or older</strong>. We do not knowingly collect information from anyone under 18. If you are under 18, please do not use this service.</p> 263 + <p>If we learn that we have collected personal information from a user under 18, we will take steps to delete that information promptly.</p> 264 + 265 + <h2>9. Changes to This Policy</h2> 266 + <p>We may update this Privacy Policy from time to time. We will notify you of significant changes by:</p> 267 + <ul> 268 + <li>Posting the new policy on this page</li> 269 + <li>Updating the "Effective Date" at the top</li> 270 + <li>Notifying you through the app when appropriate</li> 271 + </ul> 272 + <p>We encourage you to review this policy periodically.</p> 273 + 274 + <h2>10. Future Features</h2> 275 + <p>We may introduce analytics, crash reporting, or other features in the future to improve the service. If we do, we will:</p> 276 + <ul> 277 + <li>Update this Privacy Policy before implementing such features</li> 278 + <li>Use privacy-respecting options where possible</li> 279 + <li>Be transparent about what data is collected and why</li> 280 + </ul> 281 + 282 + <h2>11. Contact Us</h2> 283 + <p>If you have questions about this Privacy Policy or our practices, contact us at:</p> 284 + <p> 285 + <strong>Coves Team</strong><br> 286 + Email: <a href="mailto:support@coves.social">support@coves.social</a> 287 + </p> 288 + </div> 289 + </body> 290 + </html>
+30
internal/web/templates_test.go
··· 132 132 t.Fatal("Render() should return error for nonexistent template") 133 133 } 134 134 } 135 + 136 + func TestTemplatesRender_Privacy(t *testing.T) { 137 + templates, err := NewTemplates() 138 + if err != nil { 139 + t.Fatalf("NewTemplates() error = %v", err) 140 + } 141 + 142 + w := httptest.NewRecorder() 143 + err = templates.Render(w, "privacy.html", nil) 144 + if err != nil { 145 + t.Fatalf("Render() error = %v", err) 146 + } 147 + 148 + body := w.Body.String() 149 + if !bytes.Contains([]byte(body), []byte("Privacy Policy")) { 150 + t.Error("Privacy page does not contain title") 151 + } 152 + if !bytes.Contains([]byte(body), []byte("Coves Team")) { 153 + t.Error("Privacy page does not contain company name") 154 + } 155 + if !bytes.Contains([]byte(body), []byte("support@coves.social")) { 156 + t.Error("Privacy page does not contain contact email") 157 + } 158 + if !bytes.Contains([]byte(body), []byte("atProto")) { 159 + t.Error("Privacy page does not mention atProto") 160 + } 161 + if !bytes.Contains([]byte(body), []byte("18 years of age or older")) { 162 + t.Error("Privacy page does not contain age requirement") 163 + } 164 + }