Tap is a proof-of-concept editor for screenplays formatted in Fountain markup. It stores all data in AT Protocol records.

Added ToS and privacy policy

Changed files
+323 -8
server
+9
README.md
··· 6 6 7 7 A demo server is running at https://tapapp.lol. 8 8 9 + ### Authentication 10 + 11 + Tap uses Bluesky App Passwords (not your main account password). 12 + 13 + - Enter your Bluesky handle and an App Password on the home page to sign in. 14 + - The server stores your access and refresh tokens in memory for the duration of your session. 15 + - Tokens are refreshed automatically via `com.atproto.server.refreshSession`. 16 + - You can revoke the App Password any time in your Bluesky account settings. 17 + 9 18 ### Export features 10 19 11 20 - <strong>Export</strong> builds a `.fountain` file in plain text format and triggers a download.
+22 -6
server/main.go
··· 69 69 // 1) Read record metadata 70 70 getRecURL := "https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=" + s.DID + "&collection=lol.tapapp.tap.doc&rkey=current" 71 71 req, _ := http.NewRequest(http.MethodGet, getRecURL, nil) 72 - req.Header.Set("Authorization", "Bearer " + s.AccessJWT) 72 + req.Header.Set("Authorization", "Bearer "+s.AccessJWT) 73 73 resp, err := authedDo(w, r, req) 74 74 if err != nil { 75 75 http.Error(w, "getRecord failed", http.StatusBadGateway) ··· 111 111 // 2) Download blob (retry once on transient errors) 112 112 blobURL := "https://bsky.social/xrpc/com.atproto.sync.getBlob?did=" + s.DID + "&cid=" + cid 113 113 bReq, _ := http.NewRequest(http.MethodGet, blobURL, nil) 114 - bReq.Header.Set("Authorization", "Bearer " + s.AccessJWT) 114 + bReq.Header.Set("Authorization", "Bearer "+s.AccessJWT) 115 115 bRes, err := authedDo(w, r, bReq) 116 116 if err != nil { 117 117 http.Error(w, "getBlob failed", http.StatusBadGateway) ··· 706 706 // Routes 707 707 mux.HandleFunc("/", handleIndex) 708 708 mux.HandleFunc("/about", handleAbout) 709 + mux.HandleFunc("/privacy", handlePrivacy) 710 + mux.HandleFunc("/terms", handleTerms) 709 711 mux.HandleFunc("/health", handleHealth) 710 712 mux.HandleFunc("/preview", handlePreview) 711 713 // Multi-doc (ATProto-backed) ··· 715 717 mux.HandleFunc("/atp/session", handleATPSession) 716 718 mux.HandleFunc("/atp/post", handleATPPost) 717 719 mux.HandleFunc("/atp/doc", handleATPDoc) 718 - // OAuth routes disabled (app-passwords in use) 719 - // mux.HandleFunc("/oauth/login", handleOAuthLogin) 720 - // mux.HandleFunc("/oauth/callback", handleOAuthCallback) 721 720 722 721 addr := getEnv("PORT", "8088") 723 722 log.Printf("tap (Go) server listening on http://localhost:%s", addr) ··· 767 766 Title: "About Tap", 768 767 } 769 768 render(w, "about.html", data) 769 + } 770 + 771 + func handlePrivacy(w http.ResponseWriter, r *http.Request) { 772 + data := struct { 773 + Title string 774 + }{ 775 + Title: "Privacy Policy", 776 + } 777 + render(w, "privacy.html", data) 778 + } 779 + 780 + func handleTerms(w http.ResponseWriter, r *http.Request) { 781 + data := struct { 782 + Title string 783 + }{ 784 + Title: "Terms of Service", 785 + } 786 + render(w, "terms.html", data) 770 787 } 771 788 772 789 func handleHealth(w http.ResponseWriter, r *http.Request) { ··· 934 951 } 935 952 return res, nil 936 953 } 937 -
+1 -1
server/templates/about.html
··· 39 39 40 40 </span> 41 41 <span class="footer-right"> 42 - &copy; <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> 42 + &copy; <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> • <a href="/privacy">Privacy Policy</a> • <a href="/terms">Terms of Service</a> 43 43 </span> 44 44 </p> 45 45 </footer>
+1 -1
server/templates/index.html
··· 58 58 </span> 59 59 </span> 60 60 <span class="footer-right"> 61 - &copy; <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> 61 + &copy; <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> • <a href="/privacy">Privacy Policy</a> • <a href="/terms">Terms of Service</a> 62 62 </span> 63 63 </p> 64 64 </footer>
+135
server/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"/> 6 + <title>{{ .Title }}</title> 7 + <link rel="stylesheet" href="/static/styles.css"/> 8 + <script data-goatcounter="https://tap-editor.goatcounter.com/count" 9 + async src="//gc.zgo.at/count.js"></script> 10 + </head> 11 + <body> 12 + <header class="container header"> 13 + <h1>Tap Privacy Policy</h1> 14 + <nav> 15 + <a href="/">Home</a> 16 + <span id="header-user" class="sp" style="margin-left:12px; opacity:.85; display:none"></span> 17 + <button id="header-logout" class="sp" style="display:none">Logout</button> 18 + </nav> 19 + </header> 20 + 21 + <main class="container prose"> 22 + <h2>Overview</h2> 23 + <p>Limeleaf Worker Collective, LLC ("we", "our", or "the Service") is committed to protecting your privacy. This Privacy Policy explains how we handle information when you use our Tap service.</p> 24 + 25 + <h2>Information We Don't Collect</h2> 26 + <p>We are a privacy-first service, which we define as:</p> 27 + <ul> 28 + <li>No personal data is stored on our servers</li> 29 + <li>No tracking cookies</li> 30 + <li>No advertising or marketing data collection</li> 31 + <li>No user behavior tracking</li> 32 + <li>No data sharing with third parties</li> 33 + </ul> 34 + 35 + <h2>Authentication</h2> 36 + <p>When you sign in, Tap uses Bluesky App Passwords (not your main account password):</p> 37 + <ul> 38 + <li>We never ask for or store your main Bluesky password</li> 39 + <li>The server stores your access and refresh tokens in memory for your session</li> 40 + <li>No user profile data is persisted to disk on our servers</li> 41 + <li>You can revoke the App Password at any time in your Bluesky account settings</li> 42 + </ul> 43 + 44 + <h2>Tap Document Data</h2> 45 + <p>When you create or view documents in Tap:</p> 46 + <ul> 47 + <li>Data is stored only in your AT Protocol Personal Data Server (PDS)</li> 48 + <li>We act only as a viewer/interface for your AT Protocol data</li> 49 + <li>No AT Protocol content is cached or stored by our service</li> 50 + <li>Content permissions are determined by the AT Protocol network</li> 51 + </ul> 52 + 53 + <h2>Third-Party Services</h2> 54 + <p>We use minimal third-party services:</p> 55 + <ul> 56 + <li>Fly.io: For hosting and content delivery. Fly.io may collect basic access logs (IP addresses, user agents) for security and performance purposes. See Fly.io's Privacy Policy</li> 57 + <li>Bluesky: For authentication and authorization</li> 58 + </ul> 59 + 60 + <h2>Your Rights</h2> 61 + <p>You have the right to:</p> 62 + <ul> 63 + <li>Access the service without providing personal information</li> 64 + <li>Use the service without creating an account</li> 65 + <li>Revoke your Bluesky App Password at any time</li> 66 + <li>Clear your browser data to remove any local session information</li> 67 + </ul> 68 + 69 + <h2>Data Security</h2> 70 + <p>Since we don't collect or store personal data, there is minimal security risk. However, we implement standard web security practices including HTTPS encryption for all connections.</p> 71 + 72 + <h2>Children's Privacy</h2> 73 + <p>Our service does not knowingly collect any information from children under 13. The service is designed to be used without providing any personal information.</p> 74 + 75 + <h2>Changes to This Policy</h2> 76 + <p>We may update this Privacy Policy from time to time. Changes will be posted on this page with an updated "Last updated" date.</p> 77 + 78 + <h2>Contact</h2> 79 + <p>If you have questions about this Privacy Policy, you can contact us at: <a href="mailto:info@limeleaf.coop">info@limeleaf.coop</a></p> 80 + </main> 81 + 82 + <footer class="container footer"> 83 + <p> 84 + <span class="footer-left"> 85 + 86 + </span> 87 + <span class="footer-right"> 88 + &copy; <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> • <a href="/privacy">Privacy Policy</a> • <a href="/terms">Terms of Service</a> 89 + </span> 90 + </p> 91 + </footer> 92 + <script type="module"> 93 + (async () => { 94 + const userEl = document.getElementById('header-user'); 95 + const btn = document.getElementById('header-logout'); 96 + if (!userEl || !btn) return; 97 + 98 + const show = (handle) => { 99 + userEl.textContent = handle || ''; 100 + userEl.style.display = handle ? 'inline' : 'none'; 101 + btn.style.display = handle ? 'inline-block' : 'none'; 102 + const sampleWrap = document.getElementById('footer-sample-wrap'); 103 + if (sampleWrap) sampleWrap.style.display = handle ? 'inline' : 'none'; 104 + }; 105 + const hide = () => show(''); 106 + 107 + const sampleWrap = document.getElementById('footer-sample-wrap'); 108 + try { 109 + const res = await fetch('/atp/session', { method: 'GET' }); 110 + if (res.ok && res.status !== 204) { 111 + const s = await res.json(); 112 + show(s?.handle); 113 + } else { 114 + hide(); 115 + } 116 + } catch { hide(); } 117 + 118 + window.addEventListener('atp-login', (e) => { 119 + try { show(e?.detail?.session?.handle || ''); if (sampleWrap) sampleWrap.style.display = 'inline'; } catch {} 120 + }); 121 + window.addEventListener('atp-logout', () => { hide(); if (sampleWrap) sampleWrap.style.display = 'none'; }); 122 + 123 + if (!('___tapHeaderLogoutBound' in window)) { 124 + (window).___tapHeaderLogoutBound = true; 125 + btn.addEventListener('click', async () => { 126 + try { await fetch('/atp/session', { method: 'DELETE' }); } catch {} 127 + window.dispatchEvent(new CustomEvent('atp-logout')); 128 + }); 129 + } 130 + })(); 131 + </script> 132 + </body> 133 + </html> 134 + 135 +
+155
server/templates/terms.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"/> 6 + <title>{{ .Title }}</title> 7 + <link rel="stylesheet" href="/static/styles.css"/> 8 + <script data-goatcounter="https://tap-editor.goatcounter.com/count" 9 + async src="//gc.zgo.at/count.js"></script> 10 + </head> 11 + <body> 12 + <header class="container header"> 13 + <h1>Tap Terms of Service</h1> 14 + <nav> 15 + <a href="/">Home</a> 16 + <span id="header-user" class="sp" style="margin-left:12px; opacity:.85; display:none"></span> 17 + <button id="header-logout" class="sp" style="display:none">Logout</button> 18 + </nav> 19 + </header> 20 + 21 + <main class="container prose"> 22 + <p>Last updated: September 5, 2025</p> 23 + 24 + <h2>1. Acceptance of Terms</h2> 25 + 26 + <p>By accessing or using Tap ("the Service"), you agree to be bound by these Terms of Service as set forth by Limeleaf Worker Collective, LLC ("Limeleaf"). If you do not agree to these terms, please do not use the Service.</p> 27 + 28 + <h2>2. Description of Service</h2> 29 + 30 + <p>Tap is a web-based editor and viewer for screenplay files in the Fountain format. The Service allows users to:</p> 31 + <ul> 32 + <li>Create and edit Fountain screenplays</li> 33 + <li>Export Fountain screenplays</li> 34 + <li>Store Fountain screenplays in AT Protocol collections</li> 35 + </ul> 36 + 37 + <h2>3. Acceptable Use</h2> 38 + 39 + <p>You agree to use the Service only for lawful purposes and in accordance with these Terms. You agree not to:</p> 40 + <ul> 41 + <li>Use the Service in any way that violates applicable laws or regulations</li> 42 + <li>Attempt to gain unauthorized access to any portion of the Service</li> 43 + <li>Interfere with or disrupt the Service or servers hosting the Service</li> 44 + <li>Use automated means to access the Service without our express permission</li> 45 + <li>Impersonate any person or entity</li> 46 + </ul> 47 + 48 + <h2>4. Content and Intellectual Property</h2> 49 + 50 + <p>The Service displays content that you, the user, create. We do not claim ownership of any user-generated content. The Service interface and code are open source. You must respect the intellectual property rights of content creators.</p> 51 + 52 + <h2>5. Privacy and Data</h2> 53 + 54 + <p>Your use of the Service is subject to our Privacy Policy. By using the Service, you consent to our practices described in the Privacy Policy.</p> 55 + 56 + <h2>6. Authentication</h2> 57 + 58 + <p>You authenticate using Bluesky App Passwords (not your main account password):</p> 59 + 60 + <ul> 61 + <li>You can create and revoke an App Password at any time in your Bluesky settings</li> 62 + <li>The Service uses your App Password to obtain access and refresh tokens from Bluesky</li> 63 + <li>Tokens are stored in memory on the server for the duration of your session</li> 64 + <li>We request only the minimum permissions needed to manage your Tap documents</li> 65 + </ul> 66 + 67 + <h2>7. Disclaimers and Limitations</h2> 68 + 69 + <p>THE SERVICE IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND:</p> 70 + 71 + <ul> 72 + <li>We do not guarantee uninterrupted or error-free service</li> 73 + <li>We are not responsible for content stored by users</li> 74 + <li>Use of the Service is at your own risk</li> 75 + </ul> 76 + 77 + <h2>8. Limitation of Liability</h2> 78 + 79 + <p>To the maximum extent permitted by law, we shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses resulting from your use of the Service.</p> 80 + 81 + <h2>9. Indemnification</h2> 82 + 83 + <p>You agree to indemnify and hold harmless Limeleaf Worker Collective and its operators from any claims, damages, losses, liabilities, costs, and expenses arising from your use of the Service or violation of these Terms.</p> 84 + 85 + <h2>10. Changes to Terms</h2> 86 + 87 + <p>We may modify these Terms at any time. Changes will be effective immediately upon posting to this page. Your continued use of the Service after any changes indicates your acceptance of the modified Terms.</p> 88 + 89 + <h2>11. Termination</h2> 90 + 91 + <p>We may terminate or suspend access to the Service immediately, without prior notice, for any reason, including breach of these Terms.</p> 92 + 93 + <h2>12. Governing Law</h2> 94 + 95 + <p>These Terms shall be governed by the laws of the jurisdiction of the state of New York in the United States, without regard to conflict of law principles.</p> 96 + 97 + <h2>13. Contact Information</h2> 98 + 99 + <p>For questions about these Terms of Service, please write to info@limeleaf.coop. 100 + </main> 101 + 102 + <footer class="container footer"> 103 + <p> 104 + <span class="footer-left"> 105 + 106 + </span> 107 + <span class="footer-right"> 108 + &copy; <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> • <a href="/privacy">Privacy Policy</a> • <a href="/terms">Terms of Service</a> 109 + </span> 110 + </p> 111 + </footer> 112 + <script type="module"> 113 + (async () => { 114 + const userEl = document.getElementById('header-user'); 115 + const btn = document.getElementById('header-logout'); 116 + if (!userEl || !btn) return; 117 + 118 + const show = (handle) => { 119 + userEl.textContent = handle || ''; 120 + userEl.style.display = handle ? 'inline' : 'none'; 121 + btn.style.display = handle ? 'inline-block' : 'none'; 122 + const sampleWrap = document.getElementById('footer-sample-wrap'); 123 + if (sampleWrap) sampleWrap.style.display = handle ? 'inline' : 'none'; 124 + }; 125 + const hide = () => show(''); 126 + 127 + const sampleWrap = document.getElementById('footer-sample-wrap'); 128 + try { 129 + const res = await fetch('/atp/session', { method: 'GET' }); 130 + if (res.ok && res.status !== 204) { 131 + const s = await res.json(); 132 + show(s?.handle); 133 + } else { 134 + hide(); 135 + } 136 + } catch { hide(); } 137 + 138 + window.addEventListener('atp-login', (e) => { 139 + try { show(e?.detail?.session?.handle || ''); if (sampleWrap) sampleWrap.style.display = 'inline'; } catch {} 140 + }); 141 + window.addEventListener('atp-logout', () => { hide(); if (sampleWrap) sampleWrap.style.display = 'none'; }); 142 + 143 + if (!('___tapHeaderLogoutBound' in window)) { 144 + (window).___tapHeaderLogoutBound = true; 145 + btn.addEventListener('click', async () => { 146 + try { await fetch('/atp/session', { method: 'DELETE' }); } catch {} 147 + window.dispatchEvent(new CustomEvent('atp-logout')); 148 + }); 149 + } 150 + })(); 151 + </script> 152 + </body> 153 + </html> 154 + 155 +