{"id":51504943,"url":"https://github.com/dcondrey/rideshare","last_synced_at":"2026-07-07T23:01:28.977Z","repository":{"id":367034035,"uuid":"1251715970","full_name":"dcondrey/rideshare","owner":"dcondrey","description":"Zero-dependency, self-hosted ride-sharing platform for conference attendees","archived":false,"fork":false,"pushed_at":"2026-06-24T10:02:53.000Z","size":225,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-24T10:45:34.743Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dcondrey.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":"THREAT_MODEL.md","audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":null},"created_at":"2026-05-27T21:07:55.000Z","updated_at":"2026-06-24T10:02:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dcondrey/rideshare","commit_stats":null,"previous_names":["dcondrey/rideshare"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/dcondrey/rideshare","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcondrey%2Frideshare","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcondrey%2Frideshare/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcondrey%2Frideshare/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcondrey%2Frideshare/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dcondrey","download_url":"https://codeload.github.com/dcondrey/rideshare/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcondrey%2Frideshare/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35245324,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-07T23:01:28.315Z","updated_at":"2026-07-07T23:01:28.967Z","avatar_url":"https://github.com/dcondrey.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# Event Rideshare\n\n**A self-hosted, zero-dependency ride-sharing platform for conference and event attendees.**\n\nDeploy in one command. Run it for the event. Shut it down after.\n\n[![Node 22.5+](https://img.shields.io/badge/node-%E2%89%A522.5-brightgreen)](#requirements)\n[![Zero Dependencies](https://img.shields.io/badge/npm%20deps-0-blue)](#why-zero-dependencies)\n[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](./LICENSE)\n[![CI](https://github.com/dcondrey/rideshare/actions/workflows/ci.yml/badge.svg)](https://github.com/dcondrey/rideshare/actions/workflows/ci.yml)\n\n\u003c/div\u003e\n\n---\n\n## Why does this exist?\n\nEvery conference has the same problem: hundreds of attendees flying into the same airports, heading to the same venue, on the same dates, and nobody coordinates. People pay for solo rideshares, miss connections, and waste money.\n\nEvent Rideshare gives organizers a private, self-hosted coordination tool they can spin up in minutes, brand for their event, and tear down when it's over. No accounts to create, no app to install, no vendor lock-in, no data left behind.\n\n## How it works\n\n```\n  Attendee signs in           Posts or browses rides         Claims a ride\n  with magic link     →     (offering or requesting)    →    and gets matched\n       |                           |                              |\n  Email on allowlist?        Airport, date, time,         Poster accepts/declines.\n  Rate-limited, no           seats, meetup pin,           Both sides see contact\n  enumeration possible.      notes, flexibility.          info only after match.\n```\n\n1. **Sign in** with a registered email (passwordless magic link).\n2. **Post** a ride you're offering or requesting: airport, date, time, seats, notes, and optional pickup location.\n3. **Browse and claim** rides from other attendees. When a poster accepts, both sides exchange contact details.\n4. **View the map** with the venue, meetup spots, and all active rides pinned at their pickup locations.\n5. **Organizers** get a privacy-safe insights dashboard: engagement, match rates, unmet demand by airport/date, and CSV export.\n\n## Features\n\n| Category | Details |\n|---|---|\n| **Zero dependencies** | No `npm install`. Just Node \u003e=22.5 and a single process. No build step. |\n| **Self-contained** | One Node process + one SQLite file. Nothing else to provision. |\n| **Privacy-first** | Emails stored as one-way HMAC hashes. No trackers. No third-party JS. Aggregate-only analytics with k-anonymity. |\n| **Interactive map** | Custom slippy-map renderer (12KB vanilla JS). Pan, zoom, pinch, markers with popups. Five tile styles built in. |\n| **Portable trust** | W3C Verifiable Credentials: confirmed rides mint VCs that travel with users across events. Each deployment is a `did:web` issuer; each user is a `did:key` holder. See [TRUST.md](./TRUST.md). |\n| **One-click deploy** | Docker, Railway, Render, Fly.io, or any VPS. |\n| **Event-agnostic** | Name, dates, venue, airports, brand color, logo, meetup pins, all configurable in one YAML file or live via the admin UI. |\n| **Admin dashboard** | Allowlist management, event config editor, insights with CSV export, audit log, meetup/logo management. |\n| **Dark mode** | Automatic, based on system preference. |\n\n---\n\n## Quick start\n\n```bash\ngit clone https://github.com/dcondrey/rideshare.git\ncd rideshare\ncp .env.example .env\n```\n\nGenerate secrets and edit `.env`:\n\n```bash\n# Paste each output into .env:\nopenssl rand -hex 32    # → SESSION_SECRET\nopenssl rand -hex 32    # → ALLOWLIST_SALT\n\n# Then set:\n#   ADMIN_EMAILS=you@example.com\n#   RESEND_API_KEY=re_xxx  (or SMTP_* vars)\n#   EMAIL_FROM=\"Rideshare \u003cnoreply@yourdomain.com\u003e\"\n```\n\nStart the server:\n\n```bash\nnpm start\n# → http://localhost:3000\n```\n\nFor development with auto-reload:\n\n```bash\nnpm run dev\n```\n\n### First-run checklist\n\n1. Open `http://localhost:3000` and sign in with your admin email.\n2. Go to `/admin/allowlist` and upload your attendee CSV.\n3. Customize your event at `/admin/config` (or edit `event.config.yaml`).\n4. Share the URL with attendees.\n\n### Requirements\n\n- **Node.js 22.5+** (for the built-in `node:sqlite` module; stable in Node 24+)\n- An email provider: [Resend](https://resend.com) (free tier, recommended) or any SMTP server\n\n---\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eDeploy\u003c/strong\u003e -- Docker, Railway, Render, Fly.io\u003c/summary\u003e\n\n### Docker\n\n```bash\ncp .env.example .env    # edit with your values\ndocker compose up -d\n```\n\nSQLite lives in a named volume (`rideshare-data`). Back it up with:\n\n```bash\ndocker run --rm -v rideshare-data:/data -v $PWD:/out alpine \\\n  tar czf /out/backup.tgz /data\n```\n\n### Railway\n\n[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/YOUR-TEMPLATE-ID)\n\n1. Add a **Volume** mounted at `/data`.\n2. Set environment variables in the Railway UI:\n\n| Variable | Value |\n|---|---|\n| `APP_URL` | `https://your-app.up.railway.app` |\n| `SESSION_SECRET` | 32-byte hex (use Railway's \"generate\") |\n| `ALLOWLIST_SALT` | 32-byte hex |\n| `ADMIN_EMAILS` | `you@example.com` |\n| `RESEND_API_KEY` | `re_xxx` |\n| `EMAIL_FROM` | `\"Rideshare \u003cnoreply@yourdomain.com\u003e\"` |\n| `TRUST_PROXY` | `true` |\n\n### Render\n\nPush to GitHub, then in Render: **New \u003e Blueprint** and point at this repo. The included `render.yaml` provisions the service, a 1GB disk for SQLite, and prompts for secrets.\n\n### Fly.io\n\n```bash\nfly launch --copy-config --name your-app\nfly volumes create data --size 1 --region iad\nfly secrets set \\\n  SESSION_SECRET=$(openssl rand -hex 32) \\\n  ALLOWLIST_SALT=$(openssl rand -hex 32) \\\n  ADMIN_EMAILS=you@example.com \\\n  RESEND_API_KEY=re_xxx \\\n  EMAIL_FROM='\"Rideshare \u003cnoreply@yourdomain.com\u003e\"' \\\n  APP_URL=https://your-app.fly.dev \\\n  TRUST_PROXY=true\nfly deploy\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eConfiguration\u003c/strong\u003e -- event name, dates, airports, brand, map style\u003c/summary\u003e\n\nAll event configuration lives in **`event.config.yaml`** at the project root. Edit it once before deploy, or change most fields live via `/admin/config` without restarting. JSON is also accepted (`event.config.json`).\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eFull example configuration\u003c/strong\u003e\u003c/summary\u003e\n\n```yaml\nname: IIW XL\nlongName: Internet Identity Workshop\ntagline: Find a ride. Offer a seat. Get there together.\n\ndates:\n  start: 2026-04-21\n  end: 2026-04-23\n\nvenue:\n  name: Computer History Museum\n  address: 1401 N Shoreline Blvd, Mountain View, CA\n  lat: 37.4143\n  lng: -122.0773\n\nairports:\n  - code: SFO\n    name: San Francisco Intl\n    lat: 37.6213\n    lng: -122.3790\n  - code: SJC\n    name: San Jose Mineta Intl\n    lat: 37.3639\n    lng: -121.9289\n\nmeetups:\n  - name: Hotel Avante\n    address: 860 E El Camino Real, Mountain View\n    lat: 37.3989\n    lng: -122.0822\n\nmap:\n  style: voyager\n  defaultZoom: 11\n  customTileUrl: \"\"\n  customAttribution: \"\"\n\nbrand:\n  primaryColor: \"#2563eb\"\n  logoPath: null\n\nregistrationUrl: https://internetidentityworkshop.com\nsupportEmail: support@example.com\n```\n\n\u003c/details\u003e\n\n### Logo\n\nUpload via the admin UI at `/admin/config` (stored in DB, max 200KB; SVG/PNG/WebP/JPEG, served from `/logo`), or drop a file in `public/` and set `brand.logoPath` in config. Upload takes priority.\n\n### Map styles\n\n| Style | Look | API Key |\n|---|---|---|\n| **`voyager`** (default) | Flat retro warm palette | No |\n| `positron` | Bright minimal grayscale | No |\n| `dark-matter` | Dark retro (matches dark mode) | No |\n| `toner-lite` | Black and white | Stadia (may need key) |\n| `osm` | Classic OpenStreetMap | No |\n| `custom` | Your own tile URL | Depends on provider |\n\nFor Mapbox, MapTiler, or other paid providers, choose `custom` and set `map.customTileUrl` and `map.customAttribution`.\n\nRiders can pin their pickup location by selecting a meetup spot, entering coordinates, or letting it default to the airport.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eImporting attendees\u003c/strong\u003e\u003c/summary\u003e\n\n1. Sign in as admin and visit `/admin/allowlist`.\n2. Upload or paste a CSV. Accepts a single column of emails, or any multi-column file with an `email` header.\n3. Choose **Replace** (swap entire list) or **Append** (add to existing).\n\nThe CSV is never written to disk. Each email is normalized (lowercase, trimmed, Gmail dot/plus-tag stripped) and stored as `HMAC-SHA256(email, ALLOWLIST_SALT)`. Raw emails are never persisted.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eEnvironment variables\u003c/strong\u003e\u003c/summary\u003e\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `APP_URL` | Yes | | Public URL the app is served from |\n| `SESSION_SECRET` | Yes | | 32-byte hex; signs sessions and magic links |\n| `ALLOWLIST_SALT` | Yes | | 32-byte hex; HMAC key for attendee emails |\n| `ADMIN_EMAILS` | Yes | | Comma-separated admin email addresses |\n| `EMAIL_FROM` | Yes | | RFC 5322 sender, e.g. `\"Rideshare \u003cnoreply@x.com\u003e\"` |\n| `RESEND_API_KEY` | One of | | [Resend](https://resend.com) API key (recommended) |\n| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_SECURE` | One of | | Bring-your-own SMTP |\n| `PORT` | | `3000` | HTTP port |\n| `DATABASE_PATH` | | `./data/app.db` | SQLite file path |\n| `TRUST_PROXY` | | `false` | Set `true` behind a reverse proxy |\n| `MAGIC_LINK_RATE_LIMIT` | | `5` | Max sign-in emails per address per hour |\n| `SESSION_LIFETIME_DAYS` | | `14` | Session cookie lifetime |\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eOperations\u003c/strong\u003e -- backups, wiping after the event, updating\u003c/summary\u003e\n\n### Backups\n\n```bash\nsqlite3 ./data/app.db \".backup ./data/app.db.bak\"\n```\n\nAlso back up WAL files (`app.db-wal`, `app.db-shm`) if the server is running.\n\n### Wiping after the event\n\nUse **Allowlist \u003e Wipe attendee data** in the admin UI. To remove everything (rides, claims, audit log), delete the SQLite file or the entire deployment.\n\n### Updating\n\nPull the latest source, restart the process. Schema migrations are forward-compatible (`CREATE TABLE IF NOT EXISTS`, additive `ALTER`), so no manual migration step is needed.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eProject layout\u003c/strong\u003e\u003c/summary\u003e\n\n```\n.\n├── server.js                 # HTTP server, graceful shutdown\n├── event.config.yaml         # Event-specific defaults\n├── lib/\n│   ├── config.js             # Env + YAML/JSON config loader\n│   ├── db.js                 # SQLite schema, migrations, queries\n│   ├── auth.js               # Magic-link auth + sessions\n│   ├── rides.js              # Ride + claim business logic\n│   ├── allowlist.js          # CSV parsing, hashed allowlist\n│   ├── trust.js              # DID/VC issuance + verification\n│   ├── vc.js                 # JWT signing/verification, JWS\n│   ├── did.js                # DID generation + resolution\n│   ├── crypto.js             # Ed25519, HMAC-SHA256, base58\n│   ├── email.js              # Resend HTTP + SMTP client\n│   ├── router.js             # HTTP router + middleware\n│   ├── html.js               # Auto-escaping HTML templates\n│   ├── validate.js           # Input validation\n│   ├── rate-limit.js         # In-memory token bucket\n│   ├── insights.js           # Privacy-safe aggregate metrics\n│   ├── yaml.js               # Minimal YAML parser\n│   ├── assets.js             # Logo upload/sanitization\n│   ├── meetups.js            # Meetup CRUD\n│   ├── map-styles.js         # Tile-style catalogue\n│   └── event-config.js       # File defaults + DB overrides\n├── routes/\n│   ├── auth.js               # Sign-in, magic link, sign-out\n│   ├── rides.js              # Browse, create, claim, manage\n│   ├── admin.js              # Dashboard, allowlist, config, audit\n│   ├── map.js                # Interactive map\n│   ├── trust.js              # DID binding, VC issuance, verifier\n│   ├── well-known.js         # /.well-known/did.json\n│   └── static.js             # CSS, JS, fonts, favicon\n├── public/\n│   ├── styles.css            # Responsive UI + dark mode\n│   ├── app.js                # Progressive enhancement\n│   ├── map.js                # Custom slippy-map renderer\n│   ├── trust.js              # Client-side DID/VC management\n│   ├── favicon.svg\n│   └── robots.txt\n├── tests/                    # 18 test files, unit + integration\n├── docs/\n│   ├── code-reading-guide.md\n│   ├── intentional-non-features.md\n│   └── security/             # XSS, CSRF, SSRF, timing deep-dives\n├── Dockerfile                # Single-stage, ~70MB, non-root\n├── docker-compose.yml\n├── railway.json\n├── render.yaml\n├── fly.toml\n├── biome.json                # Lint + format rules\n├── .github/workflows/ci.yml  # Tests, lint, type-check, security\n├── SECURITY.md               # Threat model + mitigations\n├── TRUST.md                  # Portable trust protocol spec\n├── RUNBOOK.md                # Operations + troubleshooting\n├── CONTRIBUTING.md            # How to contribute\n└── CHANGELOG.md\n```\n\n\u003c/details\u003e\n\n---\n\n## Why zero dependencies?\n\nThis is a deliberate architectural choice, not a stunt.\n\n- **No supply chain risk.** No `node_modules`, no transitive dependencies, no advisory fatigue. The attack surface is this code and Node.js itself.\n- **No build step.** Clone and run. No webpack, no transpiler, no bundler.\n- **No version rot.** Nothing to update, audit, or pin. The app runs the same today as it will in five years on any Node \u003e=22.5.\n- **Instant deploy.** Docker image builds in seconds. CI runs in seconds. Cold starts are instant.\n- **Auditability.** Every line of code is in this repo. Reviewers can read it all in an afternoon.\n\nThe tradeoffs are real (hand-rolled SMTP client, custom YAML parser, custom map renderer) but intentional. Each is \u003c300 lines, tested, and does exactly what this app needs.\n\n---\n\n## Portable trust\n\nEvent Rideshare implements a decentralized trust system using W3C standards:\n\n- Each deployment has a **`did:web`** identity (anchored at `/.well-known/did.json`).\n- Each user generates a **`did:key`** (Ed25519) in their browser. The private key stays in IndexedDB.\n- Confirmed rides mint **W3C Verifiable Credentials** (compact JWT, EdDSA-signed).\n- Users carry credentials to other deployments, where signatures are verified against the original issuer's DID document.\n- A public verifier playground at `/trust/verify` lets anyone inspect any credential.\n\nNo central registry. No proprietary format. No lock-in. See [TRUST.md](./TRUST.md) for the full protocol specification.\n\n---\n\n## Security\n\nSecurity is a core design constraint, not an afterthought. See [SECURITY.md](./SECURITY.md) and [THREAT_MODEL.md](./THREAT_MODEL.md) for the complete STRIDE analysis.\n\n**Key protections:**\n\n- **No plaintext emails.** Attendee emails are stored as one-way HMAC-SHA256 hashes. A stolen database reveals nothing.\n- **No enumeration.** Magic-link endpoints return identical responses for on-list and off-list emails. Rate-limited per-email and per-IP.\n- **No third-party code.** Zero client-side JS libraries. No external fonts, trackers, or analytics. CSP enforced to `default-src 'self'`.\n- **Constant-time comparisons** for all secret-derived values.\n- **Parameterized queries** everywhere. Zero SQL injection surface.\n- **Opaque sessions.** Random tokens stored server-side, revocable instantly.\n- **Audit log.** Every admin action and sensitive operation is logged with timestamp, actor, and IP.\n- **HSTS + SameSite cookies + CSP.** Defense in depth against XSS, CSRF, and downgrade attacks.\n\nFound a vulnerability? See the disclosure process in [SECURITY.md](./SECURITY.md).\n\n---\n\n## Limitations\n\n- **Single instance.** SQLite + in-memory rate limiter prevent multi-replica deployments. For event scale (a few thousand users), one small instance is more than sufficient.\n- **No real-time updates.** Server-rendered pages. No websockets.\n- **SMTP subset.** The hand-rolled SMTP client supports PLAIN/LOGIN Auth and STARTTLS (works with Postmark, SES, Mailgun, Gmail). For XOAUTH2-only providers, use Resend.\n- **`node:sqlite` warning.** Technically experimental in Node 22 (warning suppressed). Fully stable in Node 24+.\n\nFor a full list of intentional non-features (no payments, no OAuth, no real-time chat, no native app) and the reasoning behind each, see [docs/intentional-non-features.md](./docs/intentional-non-features.md).\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) for code style, RFC process, and how to submit patches.\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdcondrey%2Frideshare","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdcondrey%2Frideshare","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdcondrey%2Frideshare/lists"}