{"id":51455612,"url":"https://github.com/imlakshay08/worksheet-store-backend","last_synced_at":"2026-07-06T00:33:46.293Z","repository":{"id":369252991,"uuid":"1273287047","full_name":"imlakshay08/worksheet-store-backend","owner":"imlakshay08","description":"Production Rails API powering a live e-commerce store — dual payment gateways (Razorpay + PayPal), webhook-driven order fulfilment, secure expiring downloads, and a session-auth admin panel.","archived":false,"fork":false,"pushed_at":"2026-07-04T19:58:57.000Z","size":132,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-06T00:33:44.904Z","etag":null,"topics":["api-only","ecommerce","payment-gateway","paypal","postgresql","rails","razorpay","rest-api","ruby","ruby-on-rails"],"latest_commit_sha":null,"homepage":"https://frenchworksheethub.com","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/imlakshay08.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"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}},"created_at":"2026-06-18T11:29:50.000Z","updated_at":"2026-07-04T19:59:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/imlakshay08/worksheet-store-backend","commit_stats":null,"previous_names":["imlakshay08/worksheet-store-backend"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/imlakshay08/worksheet-store-backend","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imlakshay08%2Fworksheet-store-backend","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imlakshay08%2Fworksheet-store-backend/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imlakshay08%2Fworksheet-store-backend/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imlakshay08%2Fworksheet-store-backend/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/imlakshay08","download_url":"https://codeload.github.com/imlakshay08/worksheet-store-backend/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imlakshay08%2Fworksheet-store-backend/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35174071,"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-05T02:00:06.290Z","response_time":100,"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":["api-only","ecommerce","payment-gateway","paypal","postgresql","rails","razorpay","rest-api","ruby","ruby-on-rails"],"created_at":"2026-07-06T00:33:45.178Z","updated_at":"2026-07-06T00:33:46.263Z","avatar_url":"https://github.com/imlakshay08.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Worksheet Store — E-commerce Backend (Rails API)\n\nProduction Rails 7.1 API that powers a **live, revenue-generating** store selling\ndigital French worksheets — with **two payment rails** (Razorpay for India, PayPal\nfor international), **webhook-driven order fulfilment**, **secure expiring\ndownloads**, and a session-authenticated **admin panel**.\n\n![Ruby](https://img.shields.io/badge/Ruby-3.1-CC342D?logo=ruby\u0026logoColor=white)\n![Rails](https://img.shields.io/badge/Rails-7.1_(API--only)-CC0000?logo=rubyonrails\u0026logoColor=white)\n![PostgreSQL](https://img.shields.io/badge/PostgreSQL-Railway-4169E1?logo=postgresql\u0026logoColor=white)\n![Payments](https://img.shields.io/badge/Payments-Razorpay_%2B_PayPal-003087?logo=paypal\u0026logoColor=white)\n![Tests](https://img.shields.io/badge/tests-passing-brightgreen)\n\n\u003e 🌐 **Live:** the storefront at `frenchworksheethub.com` (a separate static app on\n\u003e GitHub Pages) consumes this API. This repository is the **backend + admin**.\n\n---\n\n## Why this project is interesting\n\nIt's a small codebase that solves the *hard* parts of real e-commerce correctly:\nmoney can't be faked, downloads can't be stolen, fulfilment survives failures, and\nprices are recorded immutably. Most of the value here is in the **engineering\ndecisions**, documented below.\n\n---\n\n## Architecture\n\nA deliberately **decoupled** design: a static storefront and a stateless JSON API,\neach deployed and scaled independently.\n\n```\n  Storefront (static, GitHub Pages)          Backend (this repo, Railway)\n  ┌───────────────────────────────┐  HTTPS   ┌──────────────────────────────────┐\n  │ HTML/CSS/JS + checkout modal   │ ───────► │ Rails 7.1 API (api_only)         │\n  │ intl-tel-input, Razorpay/PayPal│  JSON    │  • Products / Orders endpoints    │\n  └───────────────────────────────┘          │  • Payment order creation         │\n                                              │  • Signature-verified webhooks    │\n  Customer's inbox  ◄─── Resend email ─────── │  • Secure download tokens (R2)     │\n                                              │  • Session-auth admin panel (ERB) │\n  Cloudflare R2 (PDF storage) ◄───────────────│  • PostgreSQL (source of truth)   │\n                                              └──────────────────────────────────┘\n```\n\n**Payment fulfilment is webhook-driven, not browser-driven** — the single most\nimportant design choice (see below).\n\n---\n\n## Tech stack\n\n| Layer | Choice |\n|-------|--------|\n| Language / framework | Ruby 3.1, **Rails 7.1 (`config.api_only`)** |\n| Database | PostgreSQL |\n| Payments | **Razorpay** (INR) + **PayPal Orders v2** (USD) |\n| File storage | Active Storage → **Cloudflare R2** (S3-compatible) |\n| Transactional email | **Resend** |\n| Admin UI | Server-rendered ERB + Tailwind (CDN) |\n| Abuse protection | **Rack::Attack** (throttling + body-size limits) |\n| Hosting / CI | Railway (Docker), migrations run on deploy |\n| Testing | Minitest |\n\n---\n\n## Key engineering decisions \u0026 trade-offs\n\n**1. The webhook is the source of truth — the browser is not.**\nAn order is only marked paid and fulfilled inside the **signature-verified payment\nwebhook**, never from the client's success callback. A user cannot fake a purchase\nby editing JS or replaying a request; closing the browser after paying still\nfulfils the order server-to-server.\n\n**2. Fulfilment is idempotent and failure-resilient.**\nThe webhook records the payment *first*, then delivers the download email **exactly\nonce** (tracked via `download_email_sent_at`). If email delivery raises, the action\nreturns `500` so the provider **retries** — and only the email step re-runs, because\nthe payment is already recorded. Replayed/duplicate webhooks are no-ops.\n\n**3. Order amounts are snapshotted, never derived.**\nEach order stores the exact `amount_cents` + `currency` **paid at checkout**. Editing\na product's price later never rewrites historical orders or revenue — essential for\naccounting, receipts, and disputes. (This replaced an earlier design that recomputed\nfrom the live product price — a subtle but real correctness bug.)\n\n**4. Two currencies, one fulfilment path.**\nRazorpay (INR) and PayPal (USD) are parallel rails that converge on the *same*\ndownload-token + email logic. `payment_provider` + `currency` on the order drive\ndisplay and revenue, split by currency in the admin dashboard.\n\n**5. Downloads are capability tokens, not guessable URLs.**\nOn payment, an order gets a `SecureRandom.urlsafe_base64(32)` token that is\n**paid-gated, expiring (30 days), and download-count-capped (5)**. The endpoint\nredirects to a **short-lived signed R2 URL** — the file itself is never public.\nRefunds flip the order status and instantly revoke access.\n\n**6. API-only, but sessions re-enabled for the admin.**\nThe app is `api_only` for a lean public API, with cookie/session/flash middleware\n**manually re-added** so the server-rendered admin panel gets CSRF-protected,\n`Secure`+`HttpOnly` session auth — without dragging full-stack middleware onto the\ncustomer-facing JSON endpoints.\n\n---\n\n## Security\n\nHardened against the OWASP Top 10; highlights:\n\n- **Payment integrity** — HMAC webhook signature verification (Razorpay + PayPal);\n  server-controlled amounts (a buyer can't underpay); amount-mismatch logging.\n- **Input validation** — allowlist strong params; model-level format + length caps\n  on all customer input (validated at creation only, so internal updates never\n  break); parameterized queries throughout (no SQL injection); output escaping.\n- **Abuse / DoS** — Rack::Attack throttles on `/orders` and `/admin/login`\n  (layered burst + sustained), plus request-body-size rejection before parsing.\n- **Secrets** — all in Rails **encrypted credentials**; only the *public* payment\n  key IDs ever reach the browser; PII filtered from logs.\n- **Transport \u0026 sessions** — `force_ssl` (HSTS), `Secure`/`HttpOnly`/`SameSite=Lax`\n  cookies, constant-time admin credential comparison.\n- **Supply chain** — `bundler-audit` wired in for dependency CVE scanning.\n\n---\n\n## Domain model (core)\n\n```\nProduct ──\u003c Order\n  Product: title, slug, price_in_paise (INR), price_in_cents (USD), worksheet_pdf\n  Order:   customer details, payment_provider, currency, amount_cents (snapshot),\n           status (pending → paid → refunded), download_token (+expiry, +count)\n```\n\n---\n\n## Testing\n\nMinitest coverage on the money-critical paths — webhook signature rejection\n(missing/invalid → `400`), successful capture → paid + emailed, refund → download\nrevoked, and the admin \"verify-with-provider \u0026 fulfil\" reconciliation flow.\n\n```bash\nbin/rails test\n```\n\n---\n\n## Local development\n\n```bash\ngit clone \u003cthis-repo\u003e\ncd worksheet_store\nbundle install\n\n# Rails encrypted credentials are required (Razorpay/PayPal/R2/Resend keys).\n# Provide your own master key + credentials to run against real services:\n#   EDITOR=\"code --wait\" bin/rails credentials:edit\n\nbin/rails db:prepare      # create + migrate\nbin/rails server\nbin/rails test\n```\n\nAdmin panel: `/admin/login` (single-user session auth).\n\n---\n\n## Deployment\n\nContainerised (multi-stage `Dockerfile`) and deployed on **Railway**. The entrypoint\nruns `db:prepare` on boot, so **migrations apply automatically on deploy**. Encrypted\ncredentials are decrypted in production via a single `RAILS_MASTER_KEY` env var — no\nsecrets in the image or repo.\n\n---\n\n## Roadmap\n\nNext iteration (planned): migrate the storefront to **Next.js + Tailwind**, add\n**customer accounts** (email/password + Google OAuth, Rails-owned JWT), a **cart**\nwith multi-item orders, and **order history / re-download**.\n\n---\n\n## Notes\n\nBuilt as a real product for a working French tutor — a live store handling real\npayments and customer data, not a toy demo. Design docs for the payment flow,\nsecurity posture, and full system topology live alongside this README in the repo.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimlakshay08%2Fworksheet-store-backend","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fimlakshay08%2Fworksheet-store-backend","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimlakshay08%2Fworksheet-store-backend/lists"}