From 2fd248a2fe80442d04147355d52fe5432ada4027 Mon Sep 17 00:00:00 2001 From: Manas Srivastava Date: Fri, 24 Apr 2026 00:51:40 +0530 Subject: [PATCH] fix: idempotent-by-name note no longer misleads free-tier users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The idempotent path's note said "Delete it via DELETE /api/me/resources/{token} to provision a new one with this name" but handleDeleteResource is paid-tier only — free users hitting DELETE get 403 paid_tier_only. Advertising an endpoint the caller can't use is a worse UX than not advertising one. Branch on existing.tier: - paid: keep the DELETE instruction (correct, they can use it) - non-paid: point them at /pricing.html with a note about the 24h auto-expiry as the actual free-tier lifecycle. Same change in handleNewDB and handleNewWebhook. --- internal/server/handlers.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 0d5e7ed..7e01957 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -89,7 +89,15 @@ func (s *server) handleNewDB(w http.ResponseWriter, r *http.Request) { "connection_url": existing.connectionURL, "tier": existing.tier, "limits": map[string]any{"storage_mb": s.cfg.Postgres.StorageMB, "connections": s.cfg.Postgres.ConnLimit}, - "note": fmt.Sprintf("Returning your existing %q database. Delete it via DELETE /api/me/resources/%s to provision a new one with this name.", name, existing.token), + } + // Paid users can DELETE; free users can't (DELETE returns 403 + // paid_tier_only), so don't suggest it on the free-tier path — + // that would send them to an endpoint they can't use. Steer them + // to upgrade instead. + if existing.tier == "paid" { + resp["note"] = fmt.Sprintf("Returning your existing %q database. Delete it via DELETE /api/me/resources/%s to provision a new one with this name.", name, existing.token) + } else { + resp["note"] = fmt.Sprintf("Returning your existing %q database. Free-tier resources auto-expire in 24h; upgrade to Developer for manual delete + re-provision: %s/pricing.html", name, s.marketingURL) } if existing.expiresAt.Valid { resp["expires_at"] = existing.expiresAt.Time @@ -252,7 +260,13 @@ func (s *server) handleNewWebhook(w http.ResponseWriter, r *http.Request) { "receive_url": existing.connectionURL, "tier": existing.tier, "limits": map[string]any{"requests_stored": s.cfg.Limits.WebhookMaxStored}, - "note": fmt.Sprintf("Returning your existing %q webhook. Delete it via DELETE /api/me/resources/%s to provision a new one with this name.", name, existing.token), + } + // Same caveat as handleNewDB: don't point free-tier users at a + // DELETE endpoint they'd 403 on. + if existing.tier == "paid" { + resp["note"] = fmt.Sprintf("Returning your existing %q webhook. Delete it via DELETE /api/me/resources/%s to provision a new one with this name.", name, existing.token) + } else { + resp["note"] = fmt.Sprintf("Returning your existing %q webhook. Free-tier resources auto-expire in 24h; upgrade to Developer for manual delete + re-provision: %s/pricing.html", name, s.marketingURL) } if existing.expiresAt.Valid { resp["expires_at"] = existing.expiresAt.Time