24 KiB
Free Account Options - Architecture Analysis
Date: January 14, 2026
Status: Planning Phase
Purpose: Compare two approaches for free user onboarding with limited AI operations
Current System Architecture
1. Account & Plan System
Account Model:
- plan (FK to Plan)
- credits (Integer, current balance)
- status (trial, active, suspended, pending_payment, cancelled)
- payment_method
- usage_ahrefs_queries (monthly counter)
- usage_period_start/end
Plan Model:
- name, slug, price, billing_cycle
- is_internal (hide from public listings)
- max_sites, max_keywords, max_users, max_author_profiles
- included_credits (monthly allocation)
- extra_credit_price
- allow_credit_topup
- max_ahrefs_queries (monthly limit)
2. AI Configuration System
AIModelConfig (Global - Single Source of Truth):
- model_name (e.g., 'gpt-4o-mini', 'hidream-full')
- model_type (text/image)
- provider (openai, runware, etc.)
- is_default (one default per type)
- is_active
- cost_per_1k_input/output (text models)
- credits_per_image (image models)
- tokens_per_credit (text models)
AISettings (Per-Account Overrides):
- account (FK)
- integration_type (openai, runware)
- config (API keys, settings)
- model_preferences (per operation type)
- cost_limits (budgets)
3. Credit Tracking System
CreditTransaction:
- transaction_type (purchase, subscription, deduction, adjustment)
- amount (positive/negative)
- balance_after
- description, metadata
CreditUsageLog (Per AI Operation):
- operation_type (clustering, idea_generation, content_generation, image_generation)
- credits_used
- cost_usd
- model_used
- tokens_input/output
- site (FK for filtering)
- related_object_type/id
4. Current Registration Flow
- User registers →
RegisterSerializer.create() - If
plan_slugnot provided or = 'free':- Assigns Plan.slug='free' (must exist)
- Account.status = 'trial'
- Account.credits = plan.included_credits
- Creates CreditTransaction (initial allocation)
- User can perform AI operations until credits exhausted
📊 Option 1: Individual Free Accounts (Isolated)
Concept
Each user gets their own free account with:
- Fixed cheaper AI models (GPT-4o mini, Hidream-full)
- Low credit allocation (50-100 operations)
- Own isolated data/workspace
- Ability to upgrade to paid plan
Implementation Plan
Step 1: Create Free Plan
-- Admin action via Django Admin
INSERT INTO igny8_plans (
name, slug, price, billing_cycle,
is_featured, is_internal, is_active,
max_sites, max_users, max_keywords,
included_credits, allow_credit_topup,
max_ahrefs_queries
) VALUES (
'Free Starter', 'free', 0.00, 'monthly',
false, true, true,
1, -- max_sites: 1 site only
1, -- max_users: owner only
100, -- max_keywords: 100
100, -- included_credits: 100 credits (~50 operations)
false, -- No credit topup for free
0 -- No Ahrefs access
);
Step 2: Create AI Model Configs (If Not Exist)
-- GPT-4o Mini (cheaper text model)
INSERT INTO igny8_ai_model_config (
model_name, model_type, provider, display_name,
is_default, is_active,
cost_per_1k_input, cost_per_1k_output,
tokens_per_credit, max_tokens, context_window
) VALUES (
'gpt-4o-mini', 'text', 'openai', 'GPT-4o Mini (Fast & Efficient)',
false, true,
0.00015, 0.0006, -- Cheaper than GPT-4
1000, 16384, 128000
);
-- Hidream Full (cheaper image model)
INSERT INTO igny8_ai_model_config (
model_name, model_type, provider, display_name,
is_default, is_active,
credits_per_image, quality_tier,
square_size, landscape_size
) VALUES (
'hidream-full', 'image', 'runware', 'Hidream Full (Standard Quality)',
false, true,
1, -- 1 credit per image (cheapest)
'basic',
'1024x1024', '1280x768'
);
Step 3: Update Registration Logic
# In auth/serializers.py RegisterSerializer.create()
# No changes needed! Current logic already handles this:
if not plan_slug or plan_slug == 'free':
plan = Plan.objects.get(slug='free', is_active=True)
account_status = 'trial'
initial_credits = plan.included_credits # 100 credits
Step 4: Force Free Plan AI Models
Option A: Global Default (Simplest)
-- Set GPT-4o Mini and Hidream as defaults
UPDATE igny8_ai_model_config
SET is_default = false
WHERE model_type = 'text';
UPDATE igny8_ai_model_config
SET is_default = true
WHERE model_name = 'gpt-4o-mini';
UPDATE igny8_ai_model_config
SET is_default = false
WHERE model_type = 'image';
UPDATE igny8_ai_model_config
SET is_default = true
WHERE model_name = 'hidream-full';
Pros: Zero code changes, all free accounts inherit defaults
Cons: Affects ALL accounts (paid users too)
Option B: Per-Account AI Settings (Recommended)
# In auth/serializers.py RegisterSerializer.create()
# After account creation:
if account_status == 'trial': # Free accounts only
from igny8_core.modules.system.settings_models import AISettings
# Create AI settings for OpenAI (text)
AISettings.objects.create(
account=account,
integration_type='openai',
model_preferences={
'clustering': 'gpt-4o-mini',
'idea_generation': 'gpt-4o-mini',
'content_generation': 'gpt-4o-mini',
'optimization': 'gpt-4o-mini',
},
is_active=True
)
# Create AI settings for Runware (images)
AISettings.objects.create(
account=account,
integration_type='runware',
model_preferences={
'image_generation': 'hidream-full',
},
is_active=True
)
Pros: Free accounts locked to cheap models, paid accounts unaffected
Cons: Requires code change in registration flow
Option C: Plan-Level AI Model Configuration
# Add new field to Plan model (migration required)
class Plan(models.Model):
# ... existing fields ...
allowed_text_models = models.JSONField(
default=list,
help_text="Allowed text AI models (empty = all)"
)
allowed_image_models = models.JSONField(
default=list,
help_text="Allowed image AI models (empty = all)"
)
force_default_models = models.BooleanField(
default=False,
help_text="Force plan defaults, ignore user overrides"
)
# Update Free plan:
plan = Plan.objects.get(slug='free')
plan.allowed_text_models = ['gpt-4o-mini']
plan.allowed_image_models = ['hidream-full']
plan.force_default_models = True
plan.save()
# In AI operation logic (ai/services.py or similar):
def get_ai_model_for_account(account, operation_type):
plan = account.plan
if plan.force_default_models:
if operation_type in ['clustering', 'idea_generation', 'content_generation']:
return 'gpt-4o-mini'
elif operation_type == 'image_generation':
return 'hidream-full'
# ... existing logic for paid accounts
Pros: Centralized plan-based control, scalable
Cons: Requires migration + AI operation logic changes
Step 5: Frontend Restrictions
// In frontend, check plan limits
if (user.account.plan.slug === 'free') {
// Hide model selector (force defaults)
// Show "Upgrade for more models" message
// Disable credit topup
// Disable Ahrefs research
}
✅ Pros: Individual Free Accounts
- Full User Experience - Users get their own workspace, test all features
- Data Isolation - Private data, no cross-contamination
- Smooth Upgrade Path - Existing account → upgrade plan → keep data
- Proper Multi-Tenancy - Each account is isolated, secure
- Credit Tracking - Accurate per-user usage analytics
- Marketing Value - "100 Free Credits" sounds generous
❌ Cons: Individual Free Accounts
- Database Growth - Each user = new Account + User + potential Sites/Keywords
- Abuse Potential - Users can create multiple emails for free credits
- Complex Enforcement - Need to enforce model restrictions per account
- Storage Costs - Each account stores independent data
- Migration Complexity - If user upgrades, need to handle plan transition
Effort Estimate: Individual Free Accounts
-
Minimal Approach (Option A): 1 hour
- Create free plan via admin
- Set default models globally
- Update frontend to hide topup for free users
-
Recommended Approach (Option B): 4-6 hours
- Create free plan via admin
- Update registration to create AISettings per free account
- Update AI operation logic to read account-specific models
- Frontend: Hide model selector for free users
- Testing across all AI operations
-
Enterprise Approach (Option C): 1-2 days
- Migration: Add allowed_models fields to Plan
- Update registration flow
- Refactor AI operation logic (all modules)
- Admin UI for plan model management
- Comprehensive testing
🎭 Option 2: Shared Demo Account (Multi-User)
Concept
One demo account shared by multiple users:
- Users provide email → get "demo access" token
- Limited operations pool (50-100 per user, tracked separately)
- Shared data (users see what others created)
- Pre-configured cheaper AI models
- No upgrade path (must create new account)
Implementation Plan
Step 1: Create Demo Account
-- Create demo plan (internal)
INSERT INTO igny8_plans (
name, slug, price, billing_cycle,
is_internal, is_active,
max_sites, max_users, max_keywords,
included_credits, allow_credit_topup
) VALUES (
'Demo Access', 'demo', 0.00, 'monthly',
true, true,
1, -- 1 demo site
999, -- Unlimited demo users
50, -- Limited keywords
10000, -- Large shared pool
false
);
-- Create demo account
INSERT INTO igny8_tenants (
name, slug, owner_id, plan_id,
credits, status
) VALUES (
'IGNY8 Demo Workspace', 'igny8-demo', 1, -- owner = admin
(SELECT id FROM igny8_plans WHERE slug='demo'),
10000, 'active'
);
-- Create demo site
INSERT INTO igny8_sites (
name, url, account_id, is_active
) VALUES (
'Demo Content Site', 'https://demo.example.com',
(SELECT id FROM igny8_tenants WHERE slug='igny8-demo'),
true
);
Step 2: Create DemoUserAccess Model
# In auth/models.py
class DemoUserAccess(models.Model):
"""Track individual demo user access and limits"""
email = models.EmailField(unique=True, db_index=True)
demo_account = models.ForeignKey(
'Account',
on_delete=models.CASCADE,
related_name='demo_users'
)
access_token = models.CharField(max_length=255, unique=True)
operations_used = models.IntegerField(default=0)
operations_limit = models.IntegerField(default=50)
created_at = models.DateTimeField(auto_now_add=True)
last_accessed = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
class Meta:
db_table = 'igny8_demo_user_access'
indexes = [
models.Index(fields=['email', 'is_active']),
models.Index(fields=['access_token']),
]
def __str__(self):
return f"Demo: {self.email} ({self.operations_used}/{self.operations_limit})"
def has_operations_remaining(self):
return self.operations_used < self.operations_limit
Step 3: Migration
# migrations/0014_demo_user_access.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0013_add_plan_is_internal'),
]
operations = [
migrations.CreateModel(
name='DemoUserAccess',
fields=[
('id', models.AutoField(primary_key=True)),
('email', models.EmailField(unique=True, db_index=True)),
('demo_account', models.ForeignKey(
on_delete=models.CASCADE,
to='igny8_core_auth.Account',
related_name='demo_users'
)),
('access_token', models.CharField(max_length=255, unique=True)),
('operations_used', models.IntegerField(default=0)),
('operations_limit', models.IntegerField(default=50)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_accessed', models.DateTimeField(auto_now=True)),
('is_active', models.BooleanField(default=True)),
],
options={'db_table': 'igny8_demo_user_access'},
),
]
Step 4: Demo Access Endpoint
# In auth/views.py AuthViewSet
@action(detail=False, methods=['post'], permission_classes=[])
def request_demo_access(self, request):
"""Request demo account access with email only"""
email = request.data.get('email')
if not email:
return error_response(
error='Email is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Validate email format
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
try:
validate_email(email)
except ValidationError:
return error_response(
error='Invalid email format',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Get demo account
try:
demo_account = Account.objects.get(slug='igny8-demo', status='active')
except Account.DoesNotExist:
return error_response(
error='Demo account not configured',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
# Get or create demo user access
from .models import DemoUserAccess
import secrets
demo_user, created = DemoUserAccess.objects.get_or_create(
email=email,
demo_account=demo_account,
defaults={
'access_token': secrets.token_urlsafe(32),
'operations_limit': 50,
}
)
if not demo_user.is_active:
return error_response(
error='Demo access suspended. Please contact support.',
status_code=status.HTTP_403_FORBIDDEN,
request=request
)
if not demo_user.has_operations_remaining():
return error_response(
error='Demo operation limit reached. Please sign up for a full account.',
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
request=request
)
# Generate temporary JWT for demo account
# (Custom token that includes demo_user_id)
access_token = generate_demo_access_token(demo_account, demo_user)
return success_response(
data={
'access_token': access_token,
'demo_user': {
'email': demo_user.email,
'operations_remaining': demo_user.operations_limit - demo_user.operations_used,
'operations_limit': demo_user.operations_limit,
},
'account': {
'id': demo_account.id,
'name': demo_account.name,
'is_demo': True,
}
},
message='Demo access granted' if created else 'Welcome back to demo',
request=request
)
Step 5: Custom JWT with Demo Context
# In auth/utils.py
def generate_demo_access_token(account, demo_user):
"""Generate JWT for demo access with demo_user context"""
import jwt
from datetime import datetime, timedelta
from django.conf import settings
expiry = datetime.utcnow() + timedelta(hours=24) # 24-hour demo session
payload = {
'account_id': account.id,
'account_slug': account.slug,
'demo_user_id': demo_user.id,
'demo_user_email': demo_user.email,
'is_demo': True,
'exp': expiry,
'iat': datetime.utcnow(),
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
Step 6: Demo Operation Tracking Middleware
# In middleware/demo_tracking.py
class DemoOperationTrackingMiddleware:
"""Track demo user operations and enforce limits"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Check if demo user
if hasattr(request, 'demo_user_id'):
from igny8_core.auth.models import DemoUserAccess
demo_user = DemoUserAccess.objects.select_for_update().get(
id=request.demo_user_id
)
# Check limit before processing
if not demo_user.has_operations_remaining():
return JsonResponse({
'success': False,
'error': 'Demo operation limit reached. Please sign up for a full account.',
'upgrade_url': '/pricing'
}, status=429)
# Store demo_user in request for operation tracking
request.demo_user = demo_user
response = self.get_response(request)
return response
# Add to settings.py MIDDLEWARE
Step 7: Update AI Operation Logic
# In ai/services.py or wherever AI operations are tracked
def log_ai_operation(account, operation_type, credits_used, **kwargs):
"""Log AI operation and increment demo counter if demo user"""
from igny8_core.business.billing.models import CreditUsageLog
from django.db import transaction
with transaction.atomic():
# Create credit usage log
CreditUsageLog.objects.create(
account=account,
operation_type=operation_type,
credits_used=credits_used,
**kwargs
)
# Deduct credits from account
account.credits -= credits_used
account.save()
# If demo user, increment their personal counter
from threading import local
_request = getattr(local(), 'request', None)
if _request and hasattr(_request, 'demo_user'):
demo_user = _request.demo_user
demo_user.operations_used += 1
demo_user.save()
Step 8: Frontend Demo Flow
// New demo signup flow
async function requestDemoAccess(email: string) {
const response = await api.post('/v1/auth/request-demo-access/', { email });
if (response.success) {
// Store demo token
localStorage.setItem('demo_token', response.data.access_token);
localStorage.setItem('is_demo', 'true');
// Show demo banner
showDemoBanner({
operationsRemaining: response.data.demo_user.operations_remaining,
operationsLimit: response.data.demo_user.operations_limit,
});
// Redirect to demo workspace
router.push('/dashboard');
}
}
// Demo banner component
<DemoBanner>
<p>🎭 You're in Demo Mode - {operationsRemaining} operations remaining</p>
<Button onClick={() => router.push('/pricing')}>
Upgrade for Full Access
</Button>
</DemoBanner>
// Disable certain features in demo mode
if (isDemo) {
disableFeatures(['integrations', 'automation', 'wordpress_sync']);
showSharedDataWarning();
}
✅ Pros: Shared Demo Account
- Zero Database Growth - One account, minimal new records
- Instant Access - No account creation, just email → token
- Showcase Content - Users see real AI-generated examples from others
- Anti-Abuse - Email-based tracking, hard limits per email
- Conversion Pressure - "See others creating, sign up for your own workspace"
- Cost Efficient - Shared credit pool, bulk tracking
❌ Cons: Shared Demo Account
- No Data Privacy - All users see shared workspace (could be feature or bug)
- Complex Access Control - Need custom JWT + middleware + tracking
- No Upgrade Path - Demo token ≠ real account, must register separately
- Shared Credit Pool - If pool exhausted, demo is down for everyone
- Feature Limitations - Can't show integrations, automation, publishing
- User Confusion - "Why do I see others' content?" + "Lost my demo data!"
- Backend Complexity - New model, middleware, JWT type, operation tracking
Effort Estimate: Shared Demo Account
Full Implementation: 2-3 days
- Create demo plan + account + site (1 hour)
- Create DemoUserAccess model + migration (2 hours)
- Build request_demo_access endpoint (2 hours)
- Custom JWT generation with demo context (2 hours)
- Middleware for demo tracking + limits (3 hours)
- Update AI operation logging (2 hours)
- Frontend: Demo flow + banner + restrictions (4 hours)
- Admin: Dashboard to manage demo users (2 hours)
- Testing: Edge cases, limits, shared data (4 hours)
🎯 Recommendation
🏆 Winner: Option 1 - Individual Free Accounts (Option B)
Rationale:
- Simpler Architecture - Leverages existing multi-tenancy, no custom JWT/middleware
- Better UX - Private workspace, smooth upgrade path, feels like real product
- Faster Implementation - 4-6 hours vs 2-3 days
- Lower Risk - No shared data confusion, no new access control layer
- Marketing Win - "100 Free Credits" > "Demo Access with Shared Data"
- Scalable - If abuse becomes issue, add email verification or captcha
Implementation Checklist:
- [ ] Create 'free' plan via Django Admin
- [ ] Set: included_credits=100, max_sites=1, max_keywords=100
- [ ] Set: is_internal=true, allow_credit_topup=false
- [ ] Verify AI Model Configs exist
- [ ] GPT-4o Mini (text, cheap)
- [ ] Hidream Full (image, cheap)
- [ ] Update RegisterSerializer (auth/serializers.py)
- [ ] After account creation for trial status:
- [ ] Create AISettings for openai (text) → gpt-4o-mini
- [ ] Create AISettings for runware (images) → hidream-full
- [ ] Update Frontend
- [ ] Hide model selector for free plan
- [ ] Disable credit topup for free plan
- [ ] Show "Upgrade for more models" CTA
- [ ] Testing
- [ ] Register new free account
- [ ] Run text AI operation → verify gpt-4o-mini used
- [ ] Run image AI operation → verify hidream-full used
- [ ] Verify 100 credits allocated
- [ ] Verify upgrade flow works
🔮 Future Enhancements (Optional)
For Option 1 (Individual Free Accounts):
- Email Verification - Require verified email to prevent abuse
- Captcha - Add reCAPTCHA on free signups
- Usage Analytics - Track free-to-paid conversion rates
- Referral Credits - Give 50 bonus credits for referrals
- Time-Limited Trial - 30-day access instead of credit-limited
For Option 2 (Shared Demo - If Pursued):
- Demo Content Curation - Pre-seed with high-quality examples
- Demo Reset - Daily reset to clean state
- Anonymous Mode - Show "User A, User B" instead of emails
- Live Activity Feed - "User just generated an article about X"
- Demo Leaderboard - Gamify the experience
📚 Reference Files
Models:
/backend/igny8_core/auth/models.py- Account, Plan, User/backend/igny8_core/business/billing/models.py- AIModelConfig, CreditTransaction, CreditUsageLog/backend/igny8_core/modules/system/settings_models.py- AISettings
Registration:
/backend/igny8_core/auth/serializers.py- RegisterSerializer/backend/igny8_core/auth/views.py- AuthViewSet.register()
AI Operations:
- Check modules: clustering, ideas, content, images for credit deduction logic
✅ Decision
Recommended: Proceed with Option 1 - Individual Free Accounts (Option B)
Estimated Time: 4-6 hours
Risk Level: Low
User Experience: Excellent
Consider Option 2 only if:
- Need to showcase "collaborative" aspect
- Want zero database growth (high traffic expected)
- Marketing wants "see what others create" feature