Skip to main content

max / makenotwork

Add sandbox mode: ephemeral creator accounts for dashboard exploration Sandbox route (GET/POST /sandbox): creates 1-hour ephemeral account with SmallFiles tier, rate-limited per IP (max 3 concurrent, 2 per 30s). Seeds demo project with sample items on creation. Session support: is_sandbox field in SessionUser, propagated through all login paths (password, passkey, login link, 2FA, join wizard). Access guards: check_not_sandbox() blocks Stripe, email, follows, 2FA, passkeys, SSH keys, promo claims, broadcasts, invites, library, tips, account deletion/deactivation, support tickets, password changes. Subscription tiers: sandbox users get fake Stripe product/price IDs. Discover: exclude sandbox users from all discover queries. Public profiles: return 404 for sandbox user pages. Dashboard banners: show sandbox expiry warning on all dashboard pages. Landing page: "Try the Dashboard" CTA replaces "See Use Cases".
Co-Authored-By
Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-26 19:44 UTC
Commit: cb20647d28382b33786939f725800037c148a689
Parent: c9720b2
31 files changed, +327 insertions, -37 deletions
@@ -198,7 +198,7 @@
198 198 JOIN users u ON p.user_id = u.id
199 199 LEFT JOIN item_tags pit ON pit.item_id = i.id AND pit.is_primary = true
200 200 LEFT JOIN tags pt ON pt.id = pit.tag_id
201 - WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined'
201 + WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined' AND u.is_sandbox = FALSE
202 202 "#,
203 203 )
204 204 } else if has_search {
@@ -224,7 +224,7 @@
224 224 JOIN users u ON p.user_id = u.id
225 225 LEFT JOIN item_tags pit ON pit.item_id = i.id AND pit.is_primary = true
226 226 LEFT JOIN tags pt ON pt.id = pit.tag_id
227 - WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined'
227 + WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined' AND u.is_sandbox = FALSE
228 228 "#,
229 229 )
230 230 } else {
@@ -249,7 +249,7 @@
249 249 JOIN users u ON p.user_id = u.id
250 250 LEFT JOIN item_tags pit ON pit.item_id = i.id AND pit.is_primary = true
251 251 LEFT JOIN tags pt ON pt.id = pit.tag_id
252 - WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined'
252 + WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined' AND u.is_sandbox = FALSE
253 253 "#,
254 254 )
255 255 };
@@ -298,7 +298,8 @@
298 298 SELECT COUNT(*)
299 299 FROM items i
300 300 JOIN projects p ON i.project_id = p.id
301 - WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined'
301 + JOIN users u ON p.user_id = u.id
302 + WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined' AND u.is_sandbox = FALSE
302 303 "#,
303 304 );
304 305
@@ -354,7 +355,7 @@
354 355 JOIN users u ON p.user_id = u.id
355 356 LEFT JOIN items i ON i.project_id = p.id
356 357 LEFT JOIN project_categories pc ON pc.id = p.category_id
357 - WHERE p.is_public = true
358 + WHERE p.is_public = true AND u.is_sandbox = FALSE
358 359 "#,
359 360 )
360 361 } else if has_search {
@@ -376,7 +377,7 @@
376 377 JOIN users u ON p.user_id = u.id
377 378 LEFT JOIN items i ON i.project_id = p.id
378 379 LEFT JOIN project_categories pc ON pc.id = p.category_id
379 - WHERE p.is_public = true
380 + WHERE p.is_public = true AND u.is_sandbox = FALSE
380 381 "#,
381 382 )
382 383 } else {
@@ -397,7 +398,7 @@
397 398 JOIN users u ON p.user_id = u.id
398 399 LEFT JOIN items i ON i.project_id = p.id
399 400 LEFT JOIN project_categories pc ON pc.id = p.category_id
400 - WHERE p.is_public = true
401 + WHERE p.is_public = true AND u.is_sandbox = FALSE
401 402 "#,
402 403 )
403 404 };
@@ -462,7 +463,8 @@
462 463 r#"
463 464 SELECT COUNT(*)
464 465 FROM projects p
465 - WHERE p.is_public = true
466 + JOIN users u ON p.user_id = u.id
467 + WHERE p.is_public = true AND u.is_sandbox = FALSE
466 468 "#,
467 469 );
468 470
@@ -518,7 +520,8 @@
518 520 SELECT i.item_type as category, COUNT(*) as count
519 521 FROM items i
520 522 JOIN projects p ON i.project_id = p.id
521 - WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined'
523 + JOIN users u ON p.user_id = u.id
524 + WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined' AND u.is_sandbox = FALSE
522 525 "#,
523 526 );
524 527
@@ -588,7 +591,8 @@
588 591 COUNT(*) FILTER (WHERE i.price_cents >= 10000) as over_100
589 592 FROM items i
590 593 JOIN projects p ON i.project_id = p.id
591 - WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined'
594 + JOIN users u ON p.user_id = u.id
595 + WHERE i.is_public = true AND i.listed = true AND p.is_public = true AND i.scan_status != 'quarantined' AND u.is_sandbox = FALSE
592 596 "#,
593 597 );
594 598
@@ -209,6 +209,7 @@
209 209 let creator_tier = db::creator_tiers::get_active_creator_tier(&state.db, user.id)
210 210 .await.ok().flatten().map(|t| t.to_string());
211 211 let deactivated = user.is_deactivated();
212 + let is_sandbox = user.is_sandbox;
212 213 let session_user = SessionUser {
213 214 id: user.id,
214 215 username: user.username,
@@ -220,6 +221,7 @@
220 221 is_fan_plus,
221 222 creator_tier,
222 223 deactivated,
224 + is_sandbox,
223 225 };
224 226
225 227 login_user(&session, session_user).await?;
@@ -451,6 +453,7 @@
451 453 let creator_tier = db::creator_tiers::get_active_creator_tier(&state.db, user.id)
452 454 .await.ok().flatten().map(|t| t.to_string());
453 455 let deactivated = user.is_deactivated();
456 + let is_sandbox = user.is_sandbox;
454 457 let session_user = SessionUser {
455 458 id: user.id,
456 459 username: user.username,
@@ -462,6 +465,7 @@
462 465 is_fan_plus,
463 466 creator_tier,
464 467 deactivated,
468 + is_sandbox,
465 469 };
466 470
467 471 login_user(&session, session_user).await?;
@@ -50,6 +50,7 @@
50 50
51 51 impl_into_response!(
52 52 // Public pages
53 + SandboxTemplate,
53 54 PolicyTemplate,
54 55 IndexTemplate,
55 56 LibraryTemplate,
@@ -12,6 +12,13 @@
12 12 // Public Pages
13 13 // ============================================================================
14 14
15 + /// Sandbox info page explaining the ephemeral demo mode.
16 + #[derive(Template)]
17 + #[template(path = "pages/sandbox.html")]
18 + pub struct SandboxTemplate {
19 + pub csrf_token: CsrfTokenOption,
20 + }
21 +
15 22 /// Content policy page.
16 23 #[derive(Template)]
17 24 #[template(path = "pages/policy.html")]
@@ -20,6 +20,13 @@
20 20 {% block content %}
21 21 {% include "partials/site_header.html" %}
22 22
23 + {% if let Some(su) = session_user %}{% if su.is_sandbox %}
24 + <div style="background: #6c5ce7; color: #fff; padding: 0.75rem 1.5rem; text-align: center; font-family: var(--font-mono); font-size: 0.9rem;">
25 + Sandbox mode — your data will be deleted when the session expires.
26 + <a href="/join" style="color: #fff; text-decoration: underline; margin-left: 0.5rem;">Create an account</a> to keep your work.
27 + </div>
28 + {% endif %}{% endif %}
29 +
23 30 <div class="container">
24 31 <header>
25 32 <div class="breadcrumb">
@@ -21,6 +21,13 @@
21 21 {% block content %}
22 22 {% include "partials/site_header.html" %}
23 23
24 + {% if let Some(su) = session_user %}{% if su.is_sandbox %}
25 + <div style="background: #6c5ce7; color: #fff; padding: 0.75rem 1.5rem; text-align: center; font-family: var(--font-mono); font-size: 0.9rem;">
26 + Sandbox mode — your data will be deleted when the session expires.
27 + <a href="/join" style="color: #fff; text-decoration: underline; margin-left: 0.5rem;">Create an account</a> to keep your work.
28 + </div>
29 + {% endif %}{% endif %}
30 +
24 31 <div class="container">
25 32 <header>
26 33 <div class="breadcrumb">
@@ -25,6 +25,13 @@
25 25 {% block content %}
26 26 {% include "partials/site_header.html" %}
27 27
28 + {% if let Some(su) = session_user %}{% if su.is_sandbox %}
29 + <div class="sandbox-banner" style="background: #6c5ce7; color: #fff; padding: 0.75rem 1.5rem; text-align: center; font-family: var(--font-mono); font-size: 0.9rem;">
30 + Sandbox mode — your data will be deleted when the session expires.
31 + <a href="/join" style="color: #fff; text-decoration: underline; margin-left: 0.5rem;">Create an account</a> to keep your work.
32 + </div>
33 + {% endif %}{% endif %}
34 +
28 35 <div class="container">
29 36 {% if deactivated %}
30 37 <div style="margin-bottom: 1.5rem; padding: 1.5rem; background: var(--surface-muted); border: 1px solid var(--border-color);">
@@ -117,7 +117,7 @@
117 117
118 118 <div class="landing-cta">
119 119 <a class="big-button" href="/join">Join</a>
120 - <a class="big-button secondary" href="/use-cases">See Use Cases</a>
120 + <a class="big-button secondary" href="/sandbox">Try the Dashboard</a>
121 121 </div>
122 122
123 123 <div class="tier-section">