fix: authenticated free users are no longer treated as anonymous#7
Merged
mastermanas805 merged 1 commit intomasterfrom Apr 23, 2026
Merged
Conversation
Reproduced: passing a valid Bearer JWT to /db/new or /webhook/new returned the *same anonymous resource repeatedly*, with "tier": "anonymous" "note": "Returning your existing database. Keep it forever: ..." despite the caller already being signed in. Root cause in handlers.go: the handler gated the fingerprint-based anti-abuse dedup on `if !isPaid`, where isPaid == (auth valid && PlanTier == "paid"). An authenticated FREE-tier user has a valid JWT but PlanTier != "paid", so they fell into the anonymous branch — fingerprint dedup kicked in, ignoring their auth, and returned whatever anon resource lived on their /24 subnet today. Fix: gate the fingerprint dedup on `if !isAuthed`. Anti-abuse is for unauthenticated callers; once a user has signed in, we know who they are and can attribute resources directly. Three shapes: - not authed → anonymous tier + fingerprint dedup + 24h TTL, no owner - authed free → anonymous tier (limits/TTL) + user_id set, no dedup - authed paid → paid tier + user_id set, no TTL, no dedup Both handleNewDB and handleNewWebhook updated. Note message on the authed-free path now points at /pricing.html (upgrade) instead of /start?token=... (claim) — they're already signed in, there's nothing to claim. While in the neighbourhood, corrected several notes to use s.marketingURL for /start and /pricing.html links (they were still referencing s.baseURL which is the API host).
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.
Reproduced
Passing a valid Bearer JWT to `/db/new`:
```bash
curl -X POST https://api.instanode.dev/db/new \
-H "Authorization: Bearer " \
-H 'Content-Type: application/json' \
-d '{"name":"my-project"}'
```
returned the same anonymous resource on every call, with
`"tier":"anonymous"` and
`"note":"Returning your existing database. Keep it forever: ..."`
— even though the caller is signed in. The handler was treating the
authenticated request as unauthenticated.
Root cause
`handlers.go` gated the fingerprint anti-abuse dedup on `if !isPaid`,
where `isPaid = (authed && PlanTier == "paid")`. An authenticated
FREE user has a valid JWT but `PlanTier != "paid"`, so they fell into
the anonymous branch. The fingerprint dedup then matched any anonymous
resource on their /24 subnet and returned it — silently ignoring auth.
Fix
Switch the gate to `if !isAuthed`. Fingerprint dedup is anti-abuse for
unauthenticated traffic; once someone has signed in, we know them and
can attribute resources directly. Three ownership shapes:
Same fix in both `handleNewDB` and `handleNewWebhook`. Note message
on the authed-free path now surfaces the upgrade path
(`/pricing.html`) instead of the claim path (`/start?token=...`) —
they've already signed in, nothing to claim.
While in the neighbourhood, corrected a handful of `note` strings
that still pointed at `s.baseURL` (the API host) for /start and
/pricing links — those belong on `s.marketingURL`.
Verification
confirm each call returns a new token and connection_url,
`migrated_to_user_id` is populated, and `tier == "anonymous"`
for free users / `"paid"` for paid.