_____ _ _ _
|_ _| ___ __ | || | _ _ | |__
| | / -_) / _| | __ | | +| | | '_ \
_|_|_ \___| \__|_ |_||_| \_,_| |_.__/
_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|
"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'
techub.life — AI-powered trading cards for GitHub profiles.
Rails 8 application powering AI-assisted trading cards for GitHub profiles. It bundles the GitHub App + OAuth flows, Solid Queue scheduling, Tailwind UI, and Kamal deployment so the whole experience stays operable from a laptop.
- Rails 8 + SQLite for a portable core with Solid Cache / Solid Queue / Solid Cable baked in.
- Tailwind v4 with light/dark theming, sticky header, and marketing-ready landing page.
- Composable services: every integration returns
ServiceResultobjects for predictable success/failure handling. - GitHub integrations covering App authentication, user-to-server OAuth, webhook ingestion, and profile summarisation.
- Kamal ready for containerised deploys with separate web + job hosts and SQLite volume.
- Local developer CI via
bin/ci, wiring Rubocop, Prettier, and the Rails test suite together.
-
Install dependencies the Rails way:
bin/setup --skip-server
This runs
bundle check, installs npm packages, prepares the application + Solid Queue databases, and clears temp files.
Created by Jared Hooker (@GameDevJared89) and Dean Lofts (@loftwah).
-
Boot the full stack (web, CSS watcher, Solid Queue workers, recurring scheduler):
bin/dev
-
Verify everything with the local CI pipeline:
bin/ci
- We recommend using
miseto manage tool versions across languages. This repo includes.ruby-versionand works seamlessly withmise. - If you prefer
rbenv, that also works fine. Ensure your Ruby matches.ruby-version.
Setup examples:
# Using mise (recommended)
curl https://mise.jdx.dev/install.sh | sh
mise use -g ruby@$(cat .ruby-version)
mise install
# Using rbenv
rbenv install -s $(cat .ruby-version)
rbenv local $(cat .ruby-version)
bundle install- Backups (Ops): docs/ops-backups.md
- Ops Runbook: docs/ops-runbook.md
- Storage: docs/storage.md
- Third‑Party Integrations: docs/integrations.md
- CI / CD: docs/ci-cd.md
- Ops Admin: docs/ops-admin.md
- AppSec Overview: docs/appsec-ops-overview.md
- Observability (Axiom/OTEL): docs/observability/axiom-opentelemetry.md
- Includes region support via
AXIOM_BASE_URL(US default). For CI, addAXIOM_BASE_URLsecret if you need EU.
- Includes region support via
- Image Optimization: docs/image-optimization.md
- OG Images: docs/og-images.md
- Smoke Checklist: docs/smoke-checklist.md
- ADR Index: docs/adr-index.md
- Contributing: CONTRIBUTING.md
Copy .env.example to .env and fill in the values. Key settings:
GITHUB_APP_ID,GITHUB_PRIVATE_KEYorGITHUB_PRIVATE_KEY_PATH, andGITHUB_INSTALLATION_IDfor App-based API access.GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRETfor user OAuth.GITHUB_WEBHOOK_SECRETto verify incoming webhook signatures.GITHUB_CALLBACK_URL_DEV/GITHUB_CALLBACK_URL_PRODdescribe the redirect URLs you register with GitHub. Set the dev value to your actual forwarded host (for examplehttp://127.0.0.1:3000/auth/github/callbackor the Codespaces URL) so OAuth redirects match the GitHub App settings.RESEND_API_KEYreserved for future email notifications.REQUIRE_PROFILE_ELIGIBILITY(default ON): set to0/false/noto disable the eligibility gate (e.g., paid mode). Otherwise, gating is enforced.SUBMISSION_MANUAL_INPUTS_ENABLED— deprecated; manual inputs are always enabled and fail-safe. (URL + repos) pre-steps.ACTIVE_STORAGE_SERVICE: override the storage backend (localin development/test,do_spacesin production). Set todo_spaceslocally when you want to exercise real uploads.ASSET_REDIRECT_ALLOWED_HOSTS: comma-separated hostnames allowed for off-host redirects to uploaded asset URLs (e.g., your CDN or Spaces endpoint). If unset, the app serves local copies when available and does not redirect to external hosts.IMAGE_OPT_BG_THRESHOLD: minimum file size in bytes to trigger background image optimization. Defaults to300000(≈300KB). Smaller files get a quick inline pass; larger files are optimized via a Solid Queue job on theimagesqueue and optionally re-uploaded.
The PEM dropped in the repo (techub-life.2025-10-02.private-key.pem) can be referenced via
GITHUB_PRIVATE_KEY_PATH locally.
Rails 8 expects long-lived secrets to live in config/credentials.yml.enc. Run
bin/rails credentials:edit and add a github block alongside your Active Record encryption keys,
for example:
EDITOR="cursor --wait" bin/rails credentials:edit
github:
app_id: 123456
client_id: your-oauth-client-id
client_secret: your-oauth-client-secret
private_key: |
-----BEGIN RSA PRIVATE KEY-----
paste-your-app-private-key-here
-----END RSA PRIVATE KEY-----
installation_id: your-installation-id
active_record_encryption:
primary_key: <%= `openssl rand -hex 32`.strip %>
deterministic_key: <%= `openssl rand -hex 32`.strip %>
key_derivation_salt: <%= `openssl rand -hex 32`.strip %>.env overrides still work for local experiments or CI providers that inject secrets as environment
variables. The test suite falls back to deterministic dummy encryption keys so it runs out of the
box.
The installation id comes from the GitHub UI (https://github.com/settings/installations/<id>). Set
it explicitly in credentials (github.installation_id) or via GITHUB_INSTALLATION_ID. There is no
auto-discovery or admin override; this value must be correct and stable. If you would rather point
at a file than paste the key, use the optional GITHUB_PRIVATE_KEY_PATH environment variable.
Github::AppAuthenticationServicecrafts the JWT needed for App authentication.Github::AppClientServiceissues installation tokens so we can talk to the API as the app.Github::UserOauthServiceexchanges OAuth codes, whileGithub::FetchAuthenticatedUserretrieves the authenticated user profile.Users::UpsertFromGithubpersists encrypted access tokens and profile info. Sessions are plain Rails cookies.
Login starts at /auth/github, validates the OAuth state, and stores the user id in session on
return. GitHub webhooks post to /github/webhooks; signatures are checked before dispatching events
onto Solid Queue.
Profiles::RefreshJobandProfiles::SyncFromGithubrefresh profile cards via Solid Queue.config/recurring.ymlschedules a refresh forloftwahevery 30 minutes in all environments.Github::WorkflowRunHandlerJobis wired for webhook-driven reactions (currently logs payload metadata).
Heavy image optimization runs on the images queue. The job emits structured logs for visibility:
image_optimize_started, image_optimize_skipped, image_optimize_completed, and
image_optimize_failed, including duration and size-savings metrics.
Run workers locally via the jobs and recurring processes inside Procfile.dev.
- Tailwind v4 powers the styling with a dark-mode Stimulus controller (
theme_controller.js). - Header + footer partials add navigation, theme switcher, and GitHub auth entry points.
- The home page renders Loftwah’s profile summary, top repositories, and marketing copy pulled from
service objects or the
Profilemodel cache.
- Links, achievements, and experience entries live in first-class tables (
profile_links,profile_achievements,profile_experiences). Each item supports pinning, hiding, Font Awesome icons, console “secret” codes, and Tailwind-driven style variants. - Owners manage showcase content in the
/my/profiles/:username/settingsUI. Tabs cover item CRUD, drag-free manual ordering (position fields), style presets, and console-hint tooling. Pins respect a per-profile limit (default 5) with surfaces for hero strips, spotlight grids, etc. - Public profiles render pinned items in the hero tab plus Linktree-style grids inside the Overview
tab. Hidden items stay in the DOM with badges and only appear after running
techub.revealHidden()in the browser console.techub.iddqd()adds a small glow animation. - Ahoy captures
profile_link_clicked,profile_achievement_clicked,profile_experience_clicked,hidden_items_revealed, andiddqd_triggeredevents so we can analyse how visitors interact with showcase content.
All services inherit from ApplicationService and return an instance of ServiceResult. This keeps
controller and job code honest—no nil checks, a consistent success? / failure? API, and
easy-to-test behaviour. See test/services/ for examples.
- Rubocop via
bin/rubocop(omakase config). - Prettier (
npm run prettier:check/npm run prettier:write) with Tailwind and XML plugins plus a repo-wide ignore file. - Brakeman available via
bin/brakemanfor security audits.
bin/ci orchestrates the full lint + test pipeline so you can ship green builds before asking
GitHub for CI.
The Kamal config is IP-free and single-node friendly:
servers.web.hostsreads fromWEB_HOSTS(defaults totechub.life).servers.job.hostsdefaults toWEB_HOSTS(jobs run on the same host).- Image publishes to
ghcr.io/techub-life/techub(GHCR). - Proxy host defaults to
techub.life.
- Create an SSH host alias (example):
Host techub-do
HostName <your-server-ip>
User <your-ssh-user>
IdentityFile ~/.ssh/<your-key>
- Export the only two env vars you need:
export WEB_HOSTS="techub-do" # your SSH alias
export KAMAL_REGISTRY_PASSWORD="<ghcr_token>" # packages:write scope- Deploy:
bin/kamal setup
bin/kamal deployNotes:
JOB_HOSTSis not needed; it defaults toWEB_HOSTS.REGISTRY_USERNAMEdefaults to the maintainer account; override if pushing from a different user..kamal/secretsonly referencesKAMAL_REGISTRY_PASSWORDand readsRAILS_MASTER_KEYfromconfig/master.key.
Run these on your server after deploy to validate storage and screenshots end‑to‑end:
kamal app exec -i web -- bin/rails runner 'puts({app_host: (defined?(AppHost) ? AppHost.current : nil), svc: Rails.configuration.active_storage.service}.inspect)'
kamal app exec -i web -- bin/rails runner 'puts ActiveStorage::Blob.services.fetch(Rails.configuration.active_storage.service).inspect'
kamal app exec -i web -- bin/rails runner 'b=ActiveStorage::Blob.create_and_upload!(io: StringIO.new("hi"), filename:"probe.txt"); puts b.url'
kamal app exec -i worker -- bin/rails "profiles:pipeline[loftwah,$(bin/rails runner 'print AppHost.current')]"
kamal app exec -i web -- bin/rails runner 'p Profile.for_login("loftwah").first.profile_assets.order(:created_at).pluck(:kind,:public_url,:local_path)'Spin up web + worker locally using Docker Compose:
docker compose up --build- Web: http://localhost:3000
- Worker: Solid Queue worker logs in the
workerservice - Health:
/up,/ops/jobs(if Mission Control is present)
See docs/ops-runbook.md for operations details.
After Compose is up, you can run a minimal smoke inside the web container that validates Rails
health and screenshots without external APIs:
docker compose exec -T web bash -lc "APP_HOST=http://localhost:3000 SMOKE_LOGIN=smoketest script/smoke_web.sh"What it does:
- Waits for
GET /upto be healthy - Seeds a dummy Profile offline (no GitHub calls)
- Captures an OG image via Puppeteer/Chromium to
tmp/smoke-og.png
Inspect the output file if desired:
docker compose exec -T web ls -lah /rails/tmp/smoke-og.png- Compose is used only for local production-mode smoke testing; real deployments use Kamal.
- We bypass cloud dependencies locally:
ACTIVE_STORAGE_SERVICE=localDISABLE_FORCE_SSL=1
- Rails credentials are provided by your local
config/master.keymounted into the containers.- Ensure
config/master.keyexists (generated bybin/rails credentials:edit). - Compose also accepts
RAILS_MASTER_KEYfrom your shell env as an alternative.
- Ensure
- Do not commit
config/master.keyto git.
docs/marketing-overview.mdpreserves the pre-Rails marketing write-up.docs/roadmap.mdtracks upcoming milestones so we can thin-slice future work.docs/development-workflow.mdcaptures our “one feature per PR” andServiceResultconventions.docs/application-workflow.mddescribes the end-to-end app flow (auth, sync, AI, screenshots, storage) and async orchestration.docs/user-journey.mdis the authoritative end-to-end user journey (auth → submit → generate).docs/submit-manual-inputs-workflow.mddocuments the submit flow for manual inputs (URL + repos) end-to-end (spec-first; implementation partially scaffolded).docs/status-dashboard.mdshows current implementation status per module.docs/debugging-guide.mdexplains how to pinpoint issues across stages and where artifacts live.docs/ops-admin.mddocuments ops and job administration (access, credentials, and common tasks).docs/email.mddocuments email (Resend) setup, zsh-friendly smoke tests, and ops auth.docs/definition-of-done.mdshows how we write DoD and examples of “what good looks like”.docs/eligibility-policy.mddetails the default-on eligibility policy (signals, scoring, override).components/andpages/contain early ideation notes.
Questions? Drop an issue or DM @loftwah. Happy shipping!