Skip to content

Comments

Rewrite backend from FastAPI/Python to PocketBase/Go#77

Open
JimScope wants to merge 44 commits intomainfrom
feat/pocketbase-rewrite
Open

Rewrite backend from FastAPI/Python to PocketBase/Go#77
JimScope wants to merge 44 commits intomainfrom
feat/pocketbase-rewrite

Conversation

@JimScope
Copy link
Owner

@JimScope JimScope commented Feb 23, 2026

Summary

Complete rewrite of the Ender backend from Python/FastAPI to Go/PocketBase, eliminating PostgreSQL, QStash, and multiple external dependencies while adding new capabilities.

What changed

  • Backend: ~9,300 lines of Python → ~3,000 lines of Go (-68%)
  • Frontend: 20,941 → 13,856 LOC (-34%), removed auto-generated OpenAPI client in favor of PocketBase JS SDK
  • Infrastructure: 5 Docker services → 1 single binary, 49 env vars → 20
  • Dependencies: 23 direct Python packages → 5 Go modules, ~307 transitive → ~76

Eliminated

  • PostgreSQL (replaced by embedded SQLite)
  • Upstash QStash job queue (replaced by Go goroutines)
  • Maileroo email provider (SMTP via PocketBase built-in)
  • Tropipay payment provider
  • Sentry SDK
  • Alembic migrations (10 files → 1 PocketBase migration)
  • Auto-generated OpenAPI client (replaced by PocketBase JS SDK)
  • FastAPI template remnants (issue templates, CI workflows, SVG logos, social preview)

Added

  • Real-time SSE subscriptions (PocketBase built-in)
  • Litestream continuous SQLite backup to S3
  • PocketBase Admin UI at /_/
  • Webhook secret encryption (AES)
  • PocketBase built-in rate limiting, auth, OAuth2
  • Ender branding (:Ender: wordmark, favicon, Google Fonts)
  • Design system reference in CLAUDE.md
  • Outgoing SMS webhook events: sms_sent, sms_delivered, sms_failed
  • USB modem support via standalone Go agent (modem-agent/)
  • SMS templates — reusable saved message snippets with dedicated /templates page and template picker in Send SMS dialog
  • Scheduled SMS — one-time and recurring message scheduling with /scheduled page, cron-based dispatch, pause/resume controls
  • Multi-provider payments — Stripe integration alongside QvaPay, with billing history page

USB Modem Support (new)

  • device_type field on sms_devices (android | modem) with migration + backfill
  • SSE-based message dispatch for modem devices (no FCM dependency)
  • OnRealtimeSubscribeRequest hook guards modem/* SSE topics with device API key auth
  • GET /api/sms/pending returns device_id + pending messages (agent resolves its record ID from API key)
  • Standalone modem-agent/ Go module using xlab/at for AT commands
  • Multi-modem support (each modem runs as independent goroutine with own SSE connection)
  • Incoming SMS monitoring and reporting via dev.IncomingSms() channel
  • Frontend: device type selector in Add Device dialog, Type column with icons in device table
  • Device ownership verification on POST /api/sms/report (IDOR fix)
  • Status allowlist (sent/delivered/failed) on report endpoint
  • Real-time modem status via SSE modem-status topic (no polling) — backend broadcasts on agent connect/disconnect, frontend subscribes via pb.realtime
  • Docker supportmodem-agent/Dockerfile (multi-stage, ~15MB image) + opt-in modem profile in docker-compose (docker compose --profile modem up -d)

SMS Templates & Scheduling (new)

  • sms_templates collection — name, body, user-scoped CRUD via PocketBase collections API
  • scheduled_sms collection — one-time (scheduled_at) or recurring (cron_expression + timezone), with next_run_at computed by record hooks
  • POST /api/sms/validate-cron endpoint for frontend cron expression validation
  • Cron job (*/1 * * * *) dispatches due schedules, updates status (completed for one-time, advances next_run_at for recurring)
  • Frontend: template CRUD page, scheduled SMS CRUD page with pause/resume, template picker in Send SMS dialog, body field upgraded to textarea

Backend highlights

  • PocketBase record hooks for business logic (quota, webhooks, subscriptions)
  • FCM notifications via goroutines with timeout
  • Atomic quota updates with optimistic locking
  • Structured logging via PocketBase logger
  • Multi-stage Docker build (Alpine, ~50MB image)
  • Root .dockerignore for faster build context
  • Webhook events for full SMS lifecycle (sms_received, sms_sent, sms_delivered, sms_failed) with event-specific payloads

Frontend highlights

  • React 19 useActionState for forms
  • TanStack Router loaders instead of useEffect on mount
  • Real-time SSE via useRealtimeQuery hook
  • Proper TypeScript types (zod-inferred FormData)
  • Mint-tinted design system matching ender-homepage

CI/CD

  • Removed 4 stale Python CI workflows
  • Rewrote deploy workflows with current env vars
  • Renamed SERVER_BASE_URLAPP_URL, FRONTEND_HOSTFRONTEND_URL

Test plan

  • docker compose build — image builds successfully
  • docker compose up — app starts, serves frontend + API on :8090
  • PocketBase Admin accessible at /_/
  • User signup, login, OAuth (Google/GitHub) flow works
  • SMS send via API key + FCM notification to device
  • Real-time SSE updates in dashboard
  • Webhook delivery on incoming SMS (sms_received)
  • Webhook delivery on outgoing SMS status (sms_sent, sms_delivered, sms_failed)
  • Plan subscription via QvaPay
  • Litestream backup (with S3 env vars set)
  • go build ./... compiles cleanly
  • npm run build in frontend passes
  • Create modem device → device_type: "modem", QR code shown
  • POST /api/sms/send targeting modem → status assigned, no FCM
  • SSE subscriber on modem/<deviceId> receives message (with valid API key)
  • Unauthenticated SSE subscription to modem/* is rejected
  • GET /api/sms/pending returns device_id + assigned messages for authenticated device
  • POST /api/sms/report rejects status values other than sent/delivered/failed
  • POST /api/sms/report rejects message IDs not belonging to the authenticated device
  • Multi-modem: two modems receive messages independently via own SSE topics
  • Modem agent connects → frontend device table shows green dot instantly (SSE push)
  • Modem agent disconnects → frontend device table shows gray dot instantly (SSE push)
  • Modem agent resolves its device ID from API key on startup (no manual ID config)
  • docker compose --profile modem build modem-agent — modem-agent image builds
  • docker compose --profile modem up -d — modem-agent starts after app is healthy
  • Modem agent container connects to backend via Docker internal DNS (http://app:8090)
  • Create template → appears in templates table → select in Send SMS dialog → body fills
  • Create one-time schedule (future time) → status active → cron dispatches → status completed
  • Create recurring schedule (*/5 * * * *) → next_run_at computed → dispatches → next_run_at advances
  • Pause/resume schedule → cron skips paused / resumes active
  • POST /api/sms/validate-cron returns valid + next_run for good expressions, error for bad ones

🤖 Generated with Claude Code

JimScope and others added 28 commits February 21, 2026 21:59
Replace the Python/FastAPI backend with a Go + PocketBase implementation.
This eliminates PostgreSQL, QStash, Maileroo, and the auto-generated
OpenAPI client, replacing ~7,000 lines of Python with ~2,700 lines of Go.

Backend (backend/):
- PocketBase framework with 10 collections, auth, OAuth, admin dashboard
- SMS orchestration with FCM push via goroutines (replaces QStash)
- Quota enforcement, subscription lifecycle, payment providers (QvaPay/Tropipay)
- HMAC-SHA256 webhook delivery, API key auth middleware
- Cron jobs for monthly quota reset, renewal checks, SMS retry

Frontend (frontend/):
- All hooks rewritten from OpenAPI client to PocketBase JS SDK
- All components migrated from @/client imports to pb.collection() calls
- Removed axios, @hey-api/client-axios, @hey-api/openapi-ts dependencies

Infrastructure:
- Single Dockerfile: node build + go build + alpine (~50MB image)
- docker-compose: 2 services (down from 5)
- .env.example: ~18 vars (down from 40+)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update README (EN/ES), CLAUDE.md, deployment, development, security,
and release notes to reflect the new Go + PocketBase stack. Remove all
references to FastAPI, PostgreSQL, QStash, Maileroo, and Alembic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PocketBase creates a default 'users' auth collection on init.
The migration now finds and customizes it instead of creating a new one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Loads .env from cwd or parent directory so FIRST_SUPERUSER,
FIREBASE_SERVICE_ACCOUNT_JSON, and other env vars work without
manually exporting them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reads GITHUB_CLIENT_ID/SECRET and GOOGLE_CLIENT_ID/SECRET from env
and registers them as OAuth2 providers on the users collection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Subscribe to PocketBase SSE events via a reusable useRealtimeQuery hook
that invalidates TanStack Query cache on collection changes, so the
dashboard and SMS page update instantly without polling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace is_superuser-based rules with nil (superuser-only) for admin
collections and simpler user-scoped rules. Add explicit autodate fields
for created/updated. Allow public user registration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Defaults to mailcatcher (localhost:1025) in dev. Sets sender name/address
and reads SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD from env.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Devices and API key records have hidden key fields. Unhide them in the
OnRecordCreate hook so the generated key is returned in the response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass the correct field names expected by PocketBase (email, password,
passwordConfirm, full_name) instead of spreading the raw form data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…keys

Complete dashboard real-time coverage by subscribing to sms_devices,
webhook_configs, and api_keys collections. All user-scoped ListRules
ensure PocketBase only sends events for the authenticated user's records.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use SQL arithmetic (SET col = col + N) for quota increments/decrements
to prevent race conditions on concurrent requests. Add E.164 phone
number validation and 1600-char body limit on /api/sms/send.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix device quota inconsistency: increment count before e.Next() with
  rollback on failure so quota stays consistent with actual records
- Clear OAuth access_token from localStorage on logout
- Show error toast on OAuth failure instead of silent console.error
- Add Docker healthcheck on /api/health
- Require FIRST_SUPERUSER and FIRST_SUPERUSER_PASSWORD env vars (no
  insecure defaults)
- Log errors on API key last_used_at update instead of ignoring
- Cap webhook delivery timeout at 30 seconds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add IP-based rate limiter (60 req/min) on /api/sms/report, /incoming,
and /fcm-token to prevent API key brute force attacks.

Encrypt webhook secret_key fields with AES-GCM on create/update, decrypt
on read for HMAC signing. Key derived from WEBHOOK_ENCRYPTION_KEY env
var (falls back to FIRST_SUPERUSER_PASSWORD). Backwards compatible with
existing plaintext secrets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…leware

Replace custom in-memory rate limiter with PocketBase's native
RateLimits settings. Configures rules for device endpoints (60 req/min),
SMS send (30 req/min), FCM token (10 req/min), and general API
(300 req/min). Skips if already configured via admin UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rmat

- Add 30s context timeout to FCM dispatch goroutines to prevent leaks
- Add idempotency check on payment webhooks: skip if transaction_id
  already processed with status=completed
- Auth refresh now retries twice on network errors with exponential
  backoff, only clears auth store on 401 (not transient failures)
- Standardize error responses: QuotaError now uses "detail" field
  consistent with all other API error responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Streams WAL changes to an S3-compatible backend for continuous backup.
Enabled by setting LITESTREAM_REPLICA_URL; without it the app starts
normally with zero overhead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop the unimplemented Tropipay payment provider and PAYMENT_PROVIDER
env var, simplifying GetProvider() to only return QvaPay. The plans
handler now derives the provider name from GetProvider() instead of
an env var. Renewal charge failures now return an error and send an
email notification to the user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace FindRecordsByFilter(limit 1) with FindFirstRecordByFilter in
9 locations across middleware and services. Replace custom crypto/rand
key generation with security.RandomString. Remove trivial generateID
wrapper in sms.go.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rors, and transactions

Replace log.Printf with app.Logger() slog-based structured logging
across all services and handlers. Use types.NowDateTime() and direct
time.Time values instead of manual RFC3339 formatting. Replace manual
JSON error responses with apis.NewBadRequestError/NewUnauthorizedError
etc. Wrap multi-step payment operations (CompleteInvoicePayment,
CompleteAuthorization, ProcessRenewal) in app.RunInTransaction() for
atomicity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ult)

- verify-email: move verification from useEffect+useMutation to route loader
- oauth-callback: replace useMutation+preventDefault form with useActionState
- UpgradePlanDialog: use stable string keys instead of array index for skeletons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…table

- Replace Record<string, any> with zod-inferred FormData in onSubmit handlers
- Map AddUser form fields to UserCreate interface (confirm_password → passwordConfirm)
- Add is_active to UserCreate interface
- Fix UserTableData[] cast in admin users table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all FastAPI template references (SVGs, issue templates, workflows,
page titles), add Ender branding (favicon, icon, :Ender: wordmark logo),
load Google Fonts (Inter, Libre Baskerville), and update all documentation
to reflect current PocketBase/Litestream stack.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Point CLAUDE.md to ender-homepage's design system page and global.css
as the canonical style reference. Style changes must update the design
system first, then propagate to the dashboard's index.css.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete 4 Python-era CI workflows (test-backend, lint-backend,
test-docker-compose, latest-changes). Rewrite deploy workflows with
current env vars. Rename SERVER_BASE_URL→APP_URL and
FRONTEND_HOST→FRONTEND_URL across code, config, and docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ender Ready Ready Preview, Comment Feb 23, 2026 11:48pm

Outgoing SMS status transitions now trigger webhooks. Users can subscribe
to these events in their webhook_configs.events JSON array alongside the
existing sms_received event.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add device_type field (android/modem) to sms_devices, SSE-based
message dispatch for modem devices, a standalone Go modem agent
(modem-agent/) using xlab/at for AT commands, and frontend UI for
selecting device type. Modem agents receive message assignments in
real-time via PocketBase SSE subscriptions instead of FCM.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Guard modem/* SSE topics with OnRealtimeSubscribeRequest hook that
  validates X-API-Key matches the device ID (prevents eavesdropping)
- Send X-API-Key on SSE connection and subscription in modem agent
- Verify message belongs to authenticated device in ProcessSMSAck
  (fixes IDOR: any dk_ key could update any message's status)
- Restrict /api/sms/report status to sent, delivered, or failed
  (prevents re-dispatch via status reset to assigned/pending)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check PocketBase SubscriptionsBroker for connected modem agents
instead of persisting state to SQLite. New GET /api/sms/devices/status
endpoint returns which modems have an active SSE subscription.
Frontend shows a green/gray dot next to modem devices, polling
every 30s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 30s refetchInterval with real-time SSE subscription on the
"modem-status" topic. Backend broadcasts status on modem agent
connect/disconnect so the frontend reflects changes instantly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The GET /api/sms/devices/status endpoint is now redundant since modem
status is pushed via the "modem-status" SSE topic. Frontend subscribes
on mount and receives initial state immediately via broadcast on
subscribe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The agent was using the serial port path as the device ID for SSE
subscriptions, which never matched PocketBase record IDs. Now
GET /api/sms/pending returns the device_id in its response, and the
agent stores it on first call before connecting to SSE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add resilience features to the SMS pipeline:
- Circuit breaker (5 failures → 60s cooldown) for FCM dispatch and
  webhook delivery to prevent cascading failures
- Incoming SMS deduplication via 5-minute window check on
  device+sender+body, preventing duplicates on modem reconnect
- Retry logic with max 3 attempts, exponential backoff (15m→1h→6h),
  and permanent failure detection (invalid number, blocked, etc.)
- Retry cron increased from every 6h to every 15min (backoff controls
  actual timing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JimScope and others added 2 commits February 23, 2026 12:01
Webhook deliveries are now logged with request/response details, timing,
and status. Users can test endpoints from the UI and view delivery history.
API keys support optional expiration dates and can be rotated (old key
revoked, new key generated) without deleting integrations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover the core app flows with 22 new Playwright tests across 4 spec
files (devices, sms, webhooks, integrations). Rewrite privateApi.ts
to use PocketBase admin API directly instead of the non-existent
auto-generated client, and update user-settings.spec.ts call sites
for the new createUser signature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JimScope and others added 2 commits February 23, 2026 14:18
Failed webhook deliveries now automatically retry up to 3 times with
exponential backoff (1m, 5m, 15m). Adds a manual retry button in the
delivery logs UI and a cron job that processes the retry queue every
minute.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add reusable SMS templates with a dedicated /templates page and a
template picker in the Send SMS dialog, plus one-time and recurring
SMS scheduling with a /scheduled page and a cron job that dispatches
due messages every minute.

Backend: migration for sms_templates and scheduled_sms collections,
schedule service with cron-based dispatch, validate-cron endpoint,
and record hooks to compute next_run_at.

Frontend: template CRUD (hooks, components, route), scheduled SMS
CRUD with pause/resume (hooks, components, route), template select
in SendSMS dialog, body field upgraded to textarea, sidebar nav items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JimScope and others added 2 commits February 23, 2026 17:03
Replace singleton payment provider with a registry supporting multiple
simultaneous providers. Add Stripe provider using raw HTTP (no SDK) with
checkout sessions and webhook signature verification. Users can now
select a payment provider at checkout when multiple are configured.
Includes a new /billing route with payment history DataTable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Dockerfile and docker-compose service for the modem-agent as an
opt-in profile, so it can be deployed alongside the app with
`docker compose --profile modem up -d`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lightweight SDK packages covering the integration API key (ek_) workflow:
send SMS, check quota, and verify webhook signatures. Zero external
dependencies for JS (built-in fetch) and Go (stdlib only).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant