Anonymous sign-ins let users explore your application without creating an account. They get a real authenticated session with a unique user ID, but without providing an email, password, or any personally identifiable information. When they're ready to commit, they can link a real identity to their anonymous account—preserving all their data.
On Supabase Cloud, enabling anonymous sign-ins takes one toggle in the dashboard. On self-hosted Supabase, the dashboard toggle doesn't exist. You'll configure it through environment variables in your docker-compose.yml, just like OAuth providers.
This guide covers everything: enabling the feature, writing RLS policies that distinguish anonymous from permanent users, protecting against abuse with CAPTCHA, and converting anonymous users to full accounts.
Why Anonymous Sign-Ins Matter
Anonymous authentication solves a common product problem: friction at the door. Users want to try your app before committing their email. Anonymous sign-ins let them:
- Try before signing up: Users can explore features, save preferences, and test workflows without creating an account
- Build shopping carts: E-commerce apps can let users add items before checkout
- Play demo modes: Games and productivity apps can offer meaningful trials
- Preserve data on conversion: When users finally sign up, their anonymous user ID stays the same—all associated data comes with them
The key insight is that anonymous users are real users. They have unique IDs in auth.users, they use the authenticated Postgres role, and your existing RLS policies apply to them. The only difference is a JWT claim: is_anonymous: true.
Understanding Anonymous vs. Anon Key Access
Before configuring anonymous sign-ins, it's important to understand the difference between:
Anonymous users (what this guide covers):
- Created via
signInAnonymously() - Get a real user record in
auth.users - Use the
authenticatedPostgres role - Have JWT with
is_anonymous: trueclaim - Can be converted to permanent users
Anon API key access (default public access):
- Uses the public
anonkey - No user record created
- Uses the
anonPostgres role - Cannot be converted to a user
- Used for public data access
Anonymous sign-ins give you authenticated sessions for users who haven't identified themselves—a middle ground between public access and full registration.
Enabling Anonymous Sign-Ins in Self-Hosted Supabase
The configuration requires one environment variable in your auth (GoTrue) service.
Step 1: Update Your .env File
Add the following variable to your .env file:
ENABLE_ANONYMOUS_USERS=true
Step 2: Configure docker-compose.yml
In your docker-compose.yml, ensure the auth service includes:
auth:
container_name: supabase-auth
image: supabase/gotrue:latest
environment:
# Existing configuration...
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
If you're not using .env file references, you can set it directly:
auth:
environment:
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "true"
Step 3: Restart Your Services
Apply the configuration:
docker compose down docker compose up -d
Verify the setting by checking auth logs:
docker compose logs auth | grep -i anonymous
For a deeper dive into all GoTrue configuration options, see our environment variables guide.
Using Anonymous Sign-Ins in Your Application
With the feature enabled, authenticate anonymous users with the Supabase client:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-supabase-domain.com',
'your-anon-key'
)
// Create an anonymous session
const { data, error } = await supabase.auth.signInAnonymously()
if (error) {
console.error('Anonymous sign-in failed:', error.message)
} else {
console.log('Anonymous user ID:', data.user.id)
console.log('Is anonymous:', data.user.is_anonymous)
}
The returned user object includes is_anonymous: true. From this point, the user has a valid session and can interact with your database through RLS-protected queries.
Writing RLS Policies for Anonymous Users
Here's where things get interesting. Anonymous users use the authenticated role, which means your existing RLS policies apply to them. If you've written policies like:
create policy "Authenticated users can read posts" on posts for select to authenticated using (true);
Anonymous users can read posts too. That's often fine—but you might want to restrict certain actions to permanent users only.
Checking the is_anonymous Claim
Use the JWT claim to distinguish anonymous users:
-- Check if current user is anonymous select (auth.jwt()->>'is_anonymous')::boolean;
Example: Restrict Writes to Permanent Users
Allow anonymous users to read but not write:
-- Anyone authenticated can read create policy "All users can view products" on products for select to authenticated using (true); -- Only permanent users can insert create policy "Only permanent users can create reviews" on reviews for insert to authenticated with check ( (auth.jwt()->>'is_anonymous')::boolean is false );
Example: Allow Anonymous Users Limited Access
Let anonymous users create data they own, but restrict public actions:
-- Anonymous users can save items to their own cart create policy "Users can manage their cart" on cart_items for all to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid()); -- Only permanent users can post public comments create policy "Only permanent users can comment" on comments for insert to authenticated with check ( (auth.jwt()->>'is_anonymous')::boolean is false );
Important: Use Restrictive Policies
When combining anonymous user checks with other conditions, use restrictive policies to ensure the check is always enforced:
-- RESTRICTIVE ensures this check combines with AND, not OR create policy "Block anonymous from publishing" on articles as restrictive for insert to authenticated with check ( (auth.jwt()->>'is_anonymous')::boolean is false ); -- Permissive policy for additional conditions create policy "Users can publish their own articles" on articles for insert to authenticated with check (author_id = auth.uid());
RLS policies are permissive by default (combined with OR). A restrictive policy ensures the anonymous check is always applied. For more on RLS patterns, see our Row Level Security guide.
Protecting Against Abuse with CAPTCHA
Anonymous sign-ins have a vulnerability: bots can create unlimited anonymous users, bloating your database. Supabase enforces a default rate limit of 30 requests per hour per IP, but determined attackers can work around IP limits.
The solution is CAPTCHA. Supabase supports hCaptcha and Cloudflare Turnstile.
Configuring CAPTCHA for Self-Hosted
Add these environment variables to your .env:
# CAPTCHA Configuration SECURITY_CAPTCHA_ENABLED=true SECURITY_CAPTCHA_PROVIDER=turnstile # or hcaptcha SECURITY_CAPTCHA_SECRET=your-captcha-secret-key SECURITY_CAPTCHA_TIMEOUT=10s
Update your docker-compose.yml auth service:
auth:
environment:
GOTRUE_SECURITY_CAPTCHA_ENABLED: ${SECURITY_CAPTCHA_ENABLED}
GOTRUE_SECURITY_CAPTCHA_PROVIDER: ${SECURITY_CAPTCHA_PROVIDER}
GOTRUE_SECURITY_CAPTCHA_SECRET: ${SECURITY_CAPTCHA_SECRET}
GOTRUE_SECURITY_CAPTCHA_TIMEOUT: ${SECURITY_CAPTCHA_TIMEOUT}
Client-Side CAPTCHA Integration
When CAPTCHA is enabled, you must include the token with anonymous sign-ins:
// Using Cloudflare Turnstile
const turnstileToken = await getTurnstileToken()
const { data, error } = await supabase.auth.signInAnonymously({
options: {
captchaToken: turnstileToken
}
})
For Turnstile, you'll need the site key (public) on the frontend and the secret key (private) in your GoTrue configuration.
Invisible CAPTCHA
Both hCaptcha and Turnstile support invisible mode—users never see a challenge unless they're suspicious. This provides protection without friction:
// Turnstile invisible widget <div class="cf-turnstile" data-sitekey="your-site-key" data-callback="onTurnstileSuccess" data-size="invisible" ></div>
For applications where anonymous sign-ins are a core feature, CAPTCHA protection is essential.
Converting Anonymous Users to Permanent Accounts
The real power of anonymous sign-ins is conversion. When users are ready to create a full account, they link an identity to their existing user:
Enable Manual Linking
First, enable manual linking in your auth configuration:
auth:
environment:
GOTRUE_SECURITY_MANUAL_LINKING_ENABLED: "true"
Link Email/Password Identity
const { data, error } = await supabase.auth.updateUser({
email: '[email protected]',
password: 'securepassword123'
})
if (!error) {
// User is now permanent
// is_anonymous becomes false
// User ID remains the same
}
Link OAuth Identity
const { data, error } = await supabase.auth.linkIdentity({
provider: 'google'
})
After linking, the user's is_anonymous claim becomes false, but their user ID stays the same. All data associated with that user ID is preserved—shopping carts, preferences, draft content, everything.
Handle Email Verification
When linking an email identity, the user must verify their email. Handle this in your UI:
const { data, error } = await supabase.auth.updateUser({
email: '[email protected]'
})
if (!error) {
// Email verification sent
// Show message to check inbox
}
Security Considerations
Anonymous sign-ins don't decrease your project's security, but they require careful RLS policies.
Audit Your Existing Policies
Before enabling anonymous sign-ins, review every RLS policy that grants access to authenticated users. Ask: "Should anonymous users have this access?"
Implement Rate Limits
Beyond CAPTCHA, consider application-level rate limits:
-- Example: Limit anonymous users to 10 cart items create policy "Anonymous cart item limit" on cart_items for insert to authenticated with check ( (auth.jwt()->>'is_anonymous')::boolean is false OR (select count(*) from cart_items where user_id = auth.uid()) < 10 );
Monitor Anonymous User Growth
Track anonymous user creation to detect abuse:
-- Count anonymous users created in last 24 hours select count(*) from auth.users where is_anonymous = true and created_at > now() - interval '24 hours';
Set up alerts if this number grows unexpectedly.
Consider Expiration
Anonymous sessions can accumulate. Consider a cleanup job for stale anonymous users who never convert:
-- Delete anonymous users with no activity for 30 days delete from auth.users where is_anonymous = true and last_sign_in_at < now() - interval '30 days';
Run this carefully—ensure you're not deleting users with valuable associated data.
Managing Anonymous Sign-Ins with Supascale
Editing environment variables and restarting containers works, but Supascale provides a cleaner approach for managing authentication settings across your self-hosted instances.
With Supascale's auth configuration UI, you can:
- Toggle anonymous sign-ins without editing YAML
- Configure CAPTCHA through a visual interface
- Monitor user growth including anonymous vs. permanent breakdowns
- Manage multiple projects with different auth configurations
For teams running multiple self-hosted Supabase instances, this eliminates configuration drift and reduces operational overhead. Check our pricing page to see how Supascale fits your deployment needs.
Troubleshooting
signInAnonymously Returns Error
Check that GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED is set to "true" (string, not boolean) and that you've restarted the auth container.
RLS Policies Block Anonymous Users
Remember that anonymous users use the authenticated role. If policies check for specific user metadata or identities, anonymous users will fail those checks.
CAPTCHA Token Invalid
Verify your CAPTCHA secret key is correctly set in the environment variables. The site key goes in your frontend; the secret key goes in GoTrue configuration.
Users Can't Convert to Permanent
Ensure GOTRUE_SECURITY_MANUAL_LINKING_ENABLED is set to "true". Without this, identity linking is disabled.
Summary
Anonymous sign-ins for self-hosted Supabase require:
- Enable the feature: Set
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "true" - Review RLS policies: Anonymous users are authenticated—ensure your policies handle the
is_anonymousclaim appropriately - Add CAPTCHA protection: Prevent bot abuse with hCaptcha or Turnstile
- Enable identity linking: Let users convert to permanent accounts
- Monitor and clean up: Track anonymous user growth and expire stale sessions
Anonymous authentication is powerful for reducing signup friction while maintaining a real user model. The configuration is straightforward once you understand GoTrue's environment variable patterns.
