Rewrite backend from FastAPI/Python to PocketBase/Go#77
Open
Rewrite backend from FastAPI/Python to PocketBase/Go#77
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
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>
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Eliminated
Added
/_/:Ender:wordmark, favicon, Google Fonts)sms_sent,sms_delivered,sms_failedmodem-agent/)/templatespage and template picker in Send SMS dialog/scheduledpage, cron-based dispatch, pause/resume controlsUSB Modem Support (new)
device_typefield onsms_devices(android|modem) with migration + backfillOnRealtimeSubscribeRequesthook guardsmodem/*SSE topics with device API key authGET /api/sms/pendingreturnsdevice_id+ pending messages (agent resolves its record ID from API key)modem-agent/Go module usingxlab/atfor AT commandsdev.IncomingSms()channelPOST /api/sms/report(IDOR fix)sent/delivered/failed) on report endpointmodem-statustopic (no polling) — backend broadcasts on agent connect/disconnect, frontend subscribes viapb.realtimemodem-agent/Dockerfile(multi-stage, ~15MB image) + opt-inmodemprofile in docker-compose (docker compose --profile modem up -d)SMS Templates & Scheduling (new)
sms_templatescollection — name, body, user-scoped CRUD via PocketBase collections APIscheduled_smscollection — one-time (scheduled_at) or recurring (cron_expression+timezone), withnext_run_atcomputed by record hooksPOST /api/sms/validate-cronendpoint for frontend cron expression validation*/1 * * * *) dispatches due schedules, updates status (completed for one-time, advancesnext_run_atfor recurring)Backend highlights
.dockerignorefor faster build contextsms_received,sms_sent,sms_delivered,sms_failed) with event-specific payloadsFrontend highlights
useActionStatefor formsuseEffecton mountuseRealtimeQueryhookFormData)CI/CD
SERVER_BASE_URL→APP_URL,FRONTEND_HOST→FRONTEND_URLTest plan
docker compose build— image builds successfullydocker compose up— app starts, serves frontend + API on :8090/_/sms_received)sms_sent,sms_delivered,sms_failed)go build ./...compiles cleanlynpm run buildin frontend passesdevice_type: "modem", QR code shownPOST /api/sms/sendtargeting modem → statusassigned, no FCMmodem/<deviceId>receives message (with valid API key)modem/*is rejectedGET /api/sms/pendingreturnsdevice_id+ assigned messages for authenticated devicePOST /api/sms/reportrejects status values other than sent/delivered/failedPOST /api/sms/reportrejects message IDs not belonging to the authenticated devicedocker compose --profile modem build modem-agent— modem-agent image buildsdocker compose --profile modem up -d— modem-agent starts after app is healthyhttp://app:8090)*/5 * * * *) →next_run_atcomputed → dispatches →next_run_atadvancesPOST /api/sms/validate-cronreturns valid + next_run for good expressions, error for bad ones🤖 Generated with Claude Code