interactive intro to open social

prepare for deployment: mobile-friendly, accessible, fly.io ready

- update README: friendlier framing, remove condescending language
- add mobile responsiveness: viewport meta, responsive CSS, larger tap targets
- create Dockerfile: two-stage build for minimal production image
- make OAuth production-ready: configurable redirect URI via env var
- add fly.toml: deployment config with production OAuth callback
- create justfile: deploy and dev commands

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

+39
Dockerfile
··· 1 + # Build stage 2 + FROM rustlang/rust:nightly-slim AS builder 3 + 4 + # Install build dependencies 5 + RUN apt-get update && apt-get install -y \ 6 + pkg-config \ 7 + libssl-dev \ 8 + && rm -rf /var/lib/apt/lists/* 9 + 10 + WORKDIR /app 11 + 12 + # Copy manifests 13 + COPY Cargo.toml Cargo.lock ./ 14 + 15 + # Copy source code 16 + COPY src ./src 17 + 18 + # Build for release 19 + RUN cargo build --release 20 + 21 + # Runtime stage 22 + FROM debian:bookworm-slim 23 + 24 + # Install runtime dependencies 25 + RUN apt-get update && apt-get install -y \ 26 + ca-certificates \ 27 + libssl3 \ 28 + && rm -rf /var/lib/apt/lists/* 29 + 30 + WORKDIR /app 31 + 32 + # Copy the built binary 33 + COPY --from=builder /app/target/release/at-me /app/at-me 34 + 35 + # Expose port 36 + EXPOSE 8080 37 + 38 + # Run the binary 39 + CMD ["./at-me"]
+7 -7
README.md
··· 1 1 # @me 2 2 3 - a minimal visualization of your global identity in the atproto ecosystem. 3 + an accessible visualization of how your atproto identity connects to third-party apps. 4 4 5 5 ## what is this 6 6 7 7 in decentralized social networks, you own your identity and your data lives in your personal data server. third-party applications create records in your repository using different lexicons (data schemas). 8 8 9 - @me shows this visually: your identity at the center, surrounded by the third-party apps that have created data for you. click an app to see what lexicons it uses. 9 + @me shows this visually: your identity at the center, surrounded by the third-party apps that have created data for you. click an app to see what record types it stores, then click a record type to view the actual data. 10 10 11 - ## why 12 - 13 - most people don't understand that their bluesky posts, github-like repos, and game saves all live in the same place - their PDS. this makes it obvious. 11 + ## running locally 14 12 15 - --- 13 + ```bash 14 + cargo run 15 + ``` 16 16 17 - sign in with any atproto handle at http://localhost:8080 17 + then visit http://localhost:8080 and sign in with any atproto handle.
+20
fly.toml
··· 1 + app = "at-me" 2 + primary_region = "ord" 3 + 4 + [build] 5 + 6 + [env] 7 + OAUTH_REDIRECT_URI = "https://at-me.fly.dev/oauth/callback" 8 + 9 + [http_service] 10 + internal_port = 8080 11 + force_https = true 12 + auto_stop_machines = "suspend" 13 + auto_start_machines = true 14 + min_machines_running = 0 15 + processes = ["app"] 16 + 17 + [[vm]] 18 + memory = "256mb" 19 + cpu_kind = "shared" 20 + cpus = 1
+7
justfile
··· 1 + # deploy to fly.io 2 + deploy: 3 + fly deploy 4 + 5 + # run locally 6 + dev: 7 + cargo run
+4 -1
src/oauth.rs
··· 43 43 TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()), 44 44 )); 45 45 46 + let redirect_uri = std::env::var("OAUTH_REDIRECT_URI") 47 + .unwrap_or_else(|_| "http://127.0.0.1:8080/oauth/callback".to_string()); 48 + 46 49 Arc::new( 47 50 OAuthClient::new(OAuthClientConfig { 48 51 client_metadata: AtprotoLocalhostClientMetadata { 49 - redirect_uris: Some(vec!["http://127.0.0.1:8080/oauth/callback".to_string()]), 52 + redirect_uris: Some(vec![redirect_uri]), 50 53 scopes: Some(vec![Scope::Known(KnownScope::Atproto)]), 51 54 }, 52 55 keys: None,
+29 -3
src/templates.rs
··· 62 62 <html> 63 63 <head> 64 64 <meta charset="UTF-8"> 65 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 65 66 <title>@me</title> 66 67 <style> 67 68 * {{ margin: 0; padding: 0; box-sizing: border-box; }} ··· 128 129 position: fixed; 129 130 top: 1.5rem; 130 131 left: 1.5rem; 131 - width: 24px; 132 - height: 24px; 132 + width: 32px; 133 + height: 32px; 133 134 border-radius: 50%; 134 135 border: 1px solid var(--border); 135 136 display: flex; 136 137 align-items: center; 137 138 justify-content: center; 138 - font-size: 0.7rem; 139 + font-size: 0.75rem; 139 140 color: var(--text-light); 140 141 cursor: pointer; 141 142 transition: all 0.2s ease; 142 143 z-index: 100; 144 + -webkit-tap-highlight-color: transparent; 143 145 }} 144 146 145 147 .info:hover {{ ··· 147 149 color: var(--text); 148 150 }} 149 151 152 + @media (max-width: 768px) {{ 153 + .info {{ 154 + width: 40px; 155 + height: 40px; 156 + font-size: 0.85rem; 157 + }} 158 + }} 159 + 150 160 .info-modal {{ 151 161 position: fixed; 152 162 top: 50%; ··· 156 166 border: 2px solid var(--border); 157 167 padding: 2rem; 158 168 max-width: 500px; 169 + width: 90%; 159 170 z-index: 2000; 160 171 display: none; 161 172 border-radius: 4px; 173 + }} 174 + 175 + @media (max-width: 768px) {{ 176 + .info-modal {{ 177 + padding: 1.5rem; 178 + width: 95%; 179 + }} 180 + 181 + .info-modal h2 {{ 182 + font-size: 0.9rem; 183 + }} 184 + 185 + .info-modal p {{ 186 + font-size: 0.7rem; 187 + }} 162 188 }} 163 189 164 190 .info-modal.visible {{