{"id":50389318,"url":"https://github.com/sharanch/inkwell-complete","last_synced_at":"2026-05-30T17:03:19.604Z","repository":{"id":357786806,"uuid":"1238367056","full_name":"sharanch/inkwell-complete","owner":"sharanch","description":"Microservices blogging platform — Go services, React frontend, Kubernetes (Minikube), GitOps with ArgoCD, CI/CD via GitHub Actions. SRE/DevOps portfolio project.","archived":false,"fork":false,"pushed_at":"2026-05-14T09:11:02.000Z","size":84,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-14T10:16:46.708Z","etag":null,"topics":["argocd","devops","docker","github-actions","golang","istio","kubernetes","microservices","portfolio","postgresql","react","redis","sre"],"latest_commit_sha":null,"homepage":"","language":"Go","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/sharanch.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-05-14T03:59:30.000Z","updated_at":"2026-05-14T10:13:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sharanch/inkwell-complete","commit_stats":null,"previous_names":["sharanch/inkwell"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/sharanch/inkwell-complete","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sharanch%2Finkwell-complete","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sharanch%2Finkwell-complete/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sharanch%2Finkwell-complete/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sharanch%2Finkwell-complete/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sharanch","download_url":"https://codeload.github.com/sharanch/inkwell-complete/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sharanch%2Finkwell-complete/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33700867,"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-05-30T02:00:06.278Z","response_time":92,"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":["argocd","devops","docker","github-actions","golang","istio","kubernetes","microservices","portfolio","postgresql","react","redis","sre"],"created_at":"2026-05-30T17:03:18.614Z","updated_at":"2026-05-30T17:03:19.599Z","avatar_url":"https://github.com/sharanch.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Inkwell\n\nA privacy-first blogging platform built on a Go microservices architecture.\nNo passwords — login with a one-time code sent to your email.\n\n## Architecture\n\n```\nFrontend (React + Vite)\n      │\n      ▼\nAPI Gateway (Go, port 8080)   ← JWT validation, routing\n  ├── auth-service   :8081    ← OTP login, JWT issue/refresh\n  ├── blog-service   :8082    ← Post CRUD, likes, visibility\n  ├── feed-service   :8083    ← Personalised feed, interests\n  └── notify-service :8084    ← SMTP email (internal only)\n\nData stores\n  ├── postgres-auth   (users, OTP sessions)\n  ├── postgres-blog   (posts, likes)\n  ├── postgres-feed   (interests, feed index)\n  └── redis           (OTP TTL, feed cache)\n```\n\nEach service owns its own database (database-per-service pattern).\nServices communicate over HTTP. JWT is validated at the gateway — downstream\nservices trust the injected `X-User-ID` header.\n\n## Tech stack\n\n- **Backend**: Go 1.22, Chi router, sqlx, golang-jwt, go-redis\n- **Frontend**: React 18, Vite, Tailwind CSS, React Router\n- **Infra**: Kubernetes (minikube), Docker Compose, Postgres 16, Redis 7, Nginx\n- **Observability**: Prometheus metrics at `/metrics` (Rate, Errors, Duration) on the API gateway\n- **CI**: GitHub Actions — lint, test, build on every PR\n\n\u003e **Deployment scope:** The infra manifests (`infra/k8s/`, ArgoCD, Istio) are written to production patterns (mTLS, HPA, PDB, cert-manager) but are validated locally on Minikube. No live cloud deployment exists. For a running demo, use Option A (Docker Compose) below.\n\n---\n\n## Option A — Docker Compose (local dev)\n\n### Prerequisites\n- Docker Desktop\n\n```bash\n# 1. Enter the project\ncd inkwell-complete\n\n# 2. Copy env file\ncp .env.example .env\n\n# 3. Start everything\ndocker compose up --build\n```\n\n- Frontend: http://localhost:3000\n- API: http://localhost:8080\n\nWithout SMTP configured, OTP codes print to the notify-service logs:\n\n```bash\ndocker compose logs notify-service\n```\n\n---\n\n## Option B — Minikube (Kubernetes)\n\n### Prerequisites\n- Docker Desktop\n- [minikube](https://minikube.sigs.k8s.io/docs/start/)\n- kubectl\n\n### Step 1 — Start minikube\n\n```bash\nmake mk-start\n```\n\nStarts minikube with 4 CPUs / 6 GB RAM, enables ingress and metrics-server addons,\nand adds `inkwell.local` to `/etc/hosts` (requires sudo).\n\n### Step 2 — Build images\n\n```bash\nmake build\n```\n\nBuilds all service images directly into minikube's Docker daemon — no registry needed.\n\n### Step 3 — Create secrets\n\nApply secrets directly (do not use `make k8s-secrets` — it requires an interactive editor):\n\n```bash\ncat \u003e /tmp/inkwell-secrets.yaml \u003c\u003c 'EOF'\napiVersion: v1\nkind: Secret\nmetadata:\n  name: inkwell-jwt-secrets\n  namespace: inkwell\ntype: Opaque\nstringData:\n  JWT_SECRET: \"your-strong-jwt-secret-at-least-32-chars\"\n  JWT_REFRESH_SECRET: \"your-strong-refresh-secret-at-least-32-chars\"\n---\napiVersion: v1\nkind: Secret\nmetadata:\n  name: postgres-auth-secret\n  namespace: inkwell\ntype: Opaque\nstringData:\n  POSTGRES_PASSWORD: \"devpass\"\n  AUTH_DB_PASS: \"devpass\"\n---\napiVersion: v1\nkind: Secret\nmetadata:\n  name: postgres-blog-secret\n  namespace: inkwell\ntype: Opaque\nstringData:\n  POSTGRES_PASSWORD: \"devpass\"\n  BLOG_DB_PASS: \"devpass\"\n---\napiVersion: v1\nkind: Secret\nmetadata:\n  name: postgres-feed-secret\n  namespace: inkwell\ntype: Opaque\nstringData:\n  POSTGRES_PASSWORD: \"devpass\"\n  FEED_DB_PASS: \"devpass\"\n---\napiVersion: v1\nkind: Secret\nmetadata:\n  name: smtp-secret\n  namespace: inkwell\ntype: Opaque\nstringData:\n  SMTP_HOST: \"smtp.gmail.com\"\n  SMTP_USER: \"your-gmail@gmail.com\"\n  SMTP_PASS: \"your-app-password\"\n  FROM_EMAIL: \"noreply@yourdomain.com\"\nEOF\nkubectl apply -f /tmp/inkwell-secrets.yaml\n```\n\n### Step 4 — Deploy\n\n```bash\nmake k8s-apply\nmake k8s-ingress\n```\n\n### Step 5 — Check status\n\n```bash\nkubectl get pods -n inkwell\n```\n\nWait until all pods show `1/1 Running`. Databases take ~30s.\n\n### Access the app\n\n- Frontend: http://inkwell.local\n- API: http://inkwell.local/api/\n\n---\n\n## SMTP / Email setup\n\nOTP login codes are sent by email. Three options:\n\n### Option 1 — No SMTP (dev only)\nLeave SMTP unconfigured. OTP codes print to notify-service logs:\n```bash\nkubectl logs -n inkwell -l app=notify-service --tail=50\n# or\nmake k8s-logs svc=notify-service\n```\n\n### Option 2 — Gmail with a dedicated account\nCreate a throwaway Gmail account, enable 2FA, generate an App Password at\nhttps://myaccount.google.com/apppasswords, then:\n\n```bash\nkubectl create secret generic smtp-secret \\\n  --from-literal=SMTP_HOST=smtp.gmail.com \\\n  --from-literal=SMTP_USER=throwaway@gmail.com \\\n  --from-literal=SMTP_PASS='your app password' \\\n  --from-literal=FROM_EMAIL=noreply@yourdomain.com \\\n  -n inkwell \\\n  --dry-run=client -o yaml | kubectl apply -f -\n\nkubectl rollout restart deployment/notify-service -n inkwell\n```\n\n\u003e Use a throwaway account (not your main Gmail) so Gmail doesn't override\n\u003e the FROM_EMAIL with your default sending address.\n\n### Option 3 — Resend (recommended for custom domains)\nSign up at resend.com, verify your domain, grab an API key, then:\n\n```bash\nkubectl create secret generic smtp-secret \\\n  --from-literal=SMTP_HOST=smtp.resend.com \\\n  --from-literal=SMTP_USER=resend \\\n  --from-literal=SMTP_PASS='re_your_api_key' \\\n  --from-literal=FROM_EMAIL=noreply@yourdomain.com \\\n  -n inkwell \\\n  --dry-run=client -o yaml | kubectl apply -f -\n\nkubectl rollout restart deployment/notify-service -n inkwell\n```\n\n---\n\n## Services\n\n| Service        | Port | Responsibility                         |\n|----------------|------|----------------------------------------|\n| api-gateway    | 8080 | JWT auth, reverse proxy, rate limiting |\n| auth-service   | 8081 | OTP generation, JWT issue/refresh      |\n| blog-service   | 8082 | Post CRUD, public/private visibility   |\n| feed-service   | 8083 | Ranked feed, user interest management  |\n| notify-service | 8084 | SMTP email sending (internal only)     |\n\n## Auth flow\n\n1. User enters email → POST `/api/v1/auth/request-otp`\n2. `auth-service` generates 6-digit code, stores in Redis (10 min TTL), calls `notify-service`\n3. `notify-service` sends email via SMTP\n4. User enters code → POST `/api/v1/auth/verify-otp`\n5. `auth-service` validates code (one-time use), upserts user in Postgres\n6. Returns `access_token` (15 min) + `refresh_token` (7 days)\n7. Frontend auto-refreshes on 401\n\n---\n\n## Useful commands\n\n```bash\n# Status\nmake mk-status                          # minikube + cluster overview\nmake k8s-status                         # pods, services, ingress\n\n# Logs\nmake k8s-logs svc=auth-service          # tail pod logs\nmake k8s-logs svc=notify-service        # check OTP codes / SMTP errors\n\n# Debugging\nmake k8s-exec svc=auth-service          # shell into a pod\nmake describe svc=blog-service          # describe pods (good for crash diagnosis)\nmake forward svc=api-gateway port=8080  # port-forward for direct access\n\n# Rebuild a single service after code changes\nmake reload svc=blog-service\n\n# Database access\nmake psql db=auth                       # psql into postgres-auth\nmake redis-cli                          # redis-cli\n\n# Teardown\nmake k8s-delete                         # delete all inkwell resources (keeps minikube)\nmake mk-delete                          # delete minikube cluster entirely\n```\n\n## Local development (single service)\n\n```bash\n# Requires Postgres + Redis running locally\ncd auth-service\ngo run ./cmd/server\n\n# With hot reload (install air first: go install github.com/air-verse/air@latest)\nair\n\n# Tests\ngo test ./... -race -cover\n```\n\n---\n\n## Roadmap\n\n- [ ] Rich text editor for writing posts\n- [ ] Post detail page with comments\n- [ ] User profile page\n- [ ] Feed score updates on like/view events (currently manual)\n- [ ] WebSocket notifications\n- [ ] Full-text search (pg trigrams or Meilisearch)\n- [ ] Image upload (S3/R2)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsharanch%2Finkwell-complete","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsharanch%2Finkwell-complete","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsharanch%2Finkwell-complete/lists"}