Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
561092e
Add programmatic SEO pages, author attribution, and internal linking
sunithvs Feb 28, 2026
a111f5b
Add X (Twitter) link to author bio footer
sunithvs Feb 28, 2026
2b49305
Add blog posts on Firebase and Supabase blocking in India, including …
sunithvs Feb 28, 2026
178155a
Add 7 new blog posts, JSON-LD schema markup across all pages, and sit…
sunithvs Feb 28, 2026
f2ec970
Add Terms of Service, Privacy Policy, and legal enforcement
sunithvs Feb 28, 2026
bc14209
Fix favicon for Google Search: add ICO, PNG, manifest, and apple-touc…
sunithvs Feb 28, 2026
5a88966
Add non-affiliation disclaimer to homepage footer
sunithvs Feb 28, 2026
e1ef28f
Fix footer disclaimer layout - move below flex row
sunithvs Feb 28, 2026
59c33eb
Add client-side validation and detailed error messages for app creati…
sunithvs Feb 28, 2026
abb4ce1
Add donation modal with periodic and celebration variants
sunithvs Feb 28, 2026
0406b4a
Enhance caching and URL handling in config and handler modules
sunithvs Feb 28, 2026
7a37e44
Add donation gate component to prompt users before app creation
sunithvs Mar 1, 2026
ff8fadf
Add donation gate component to prompt users before app creation
sunithvs Mar 1, 2026
36a0bbc
Add AGPL-3.0-only license to package.json files and create LICENSE file
sunithvs Mar 1, 2026
b14a51b
Revert "Add donation gate component to prompt users before app creation"
manubhardwaj Mar 1, 2026
0326965
Revert "Add donation gate component to prompt users before app creation"
manubhardwaj Mar 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
661 changes: 661 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@jiobase/api",
"version": "0.0.1",
"private": true,
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
"dev": "wrangler dev --port 8788",
Expand Down
1 change: 1 addition & 0 deletions apps/proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@jiobase/proxy",
"version": "0.0.1",
"private": true,
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
"dev": "wrangler dev",
Expand Down
12 changes: 8 additions & 4 deletions apps/proxy/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Env, ProxyConfig } from './types.js';

// Cache KV reads for 60 seconds — avoids hitting KV on every single request.
// Config updates (from the API) take up to 60s to propagate, which is acceptable.
const KV_CACHE_TTL = 60;

export async function resolveConfig(
hostname: string,
env: Env
Expand All @@ -19,17 +23,17 @@ export async function resolveConfig(
}

if (slug) {
// Look up by slug
const raw = await env.PROXY_CONFIG.get(`app:${slug}`);
// Look up by slug with caching
const raw = await env.PROXY_CONFIG.get(`app:${slug}`, { cacheTtl: KV_CACHE_TTL });
if (!raw) return null;
return { config: JSON.parse(raw), slug };
}

// Otherwise, check custom domain mapping
const mappedSlug = await env.PROXY_CONFIG.get(`domain:${hostname}`);
const mappedSlug = await env.PROXY_CONFIG.get(`domain:${hostname}`, { cacheTtl: KV_CACHE_TTL });
if (!mappedSlug) return null;

const raw = await env.PROXY_CONFIG.get(`app:${mappedSlug}`);
const raw = await env.PROXY_CONFIG.get(`app:${mappedSlug}`, { cacheTtl: KV_CACHE_TTL });
if (!raw) return null;
return { config: JSON.parse(raw), slug: mappedSlug };
}
24 changes: 18 additions & 6 deletions apps/proxy/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,25 @@ export async function handleHttpProxy(
// Clone response headers
const responseHeaders = new Headers(upstreamResponse.headers);

// Rewrite supabase.co URLs in Location headers (redirects)
// Rewrite Location headers that redirect directly to the Supabase host.
// Only rewrite the *host* portion of the URL — NOT query params.
// This avoids breaking OAuth redirect_uri params (e.g. Google's redirect_uri
// must match exactly between the authorize request and the token exchange).
const location = responseHeaders.get('Location');
if (location && location.includes('.supabase.co')) {
responseHeaders.set(
'Location',
location.replace(new URL(config.supabaseUrl).hostname, url.hostname)
);
if (location) {
try {
const locUrl = new URL(location);
const supabaseHost = new URL(config.supabaseUrl).hostname;
if (locUrl.hostname === supabaseHost) {
// Direct redirect to Supabase — rewrite host to proxy
locUrl.hostname = url.hostname;
responseHeaders.set('Location', locUrl.toString());
}
// If Location points to an external host (e.g. accounts.google.com),
// leave it untouched — including any redirect_uri query params.
} catch {
// Malformed Location header — leave it as-is
}
}

// Add CORS headers
Expand Down
15 changes: 10 additions & 5 deletions apps/proxy/src/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ export async function handleWebSocket(
upstreamUrl.pathname = url.pathname;
upstreamUrl.search = url.search;

// Build upstream WebSocket URL
const wsUrl = upstreamUrl.toString().replace('https:', 'wss:').replace('http:', 'ws:');
// Cloudflare Workers fetch() requires https:// (not wss://) for WebSocket upgrade.
// The Upgrade header tells the upstream to switch protocols.
const headers = new Headers(request.headers);
headers.set('Host', upstreamUrl.hostname);
headers.delete('cf-connecting-ip');
headers.delete('cf-ray');
headers.delete('cf-visitor');
headers.delete('cf-ipcountry');

// Create upstream WebSocket connection
const upstreamResp = await fetch(wsUrl, {
headers: request.headers,
const upstreamResp = await fetch(upstreamUrl.toString(), {
headers,
});

const upstreamWs = upstreamResp.webSocket;
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@jiobase/web",
"private": true,
"license": "AGPL-3.0-only",
"version": "0.0.1",
"type": "module",
"scripts": {
Expand Down
63 changes: 63 additions & 0 deletions apps/web/src/lib/components/AuthorBio.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script lang="ts">
const author = {
name: 'Sunith VS',
website: 'https://sunithvs.com',
linkedin: 'https://www.linkedin.com/in/sunithvs/',
instagram: 'https://www.instagram.com/truevibecoder/',
youtube: 'https://www.youtube.com/@truevibecoder',
twitter: 'https://x.com/sunithvs_'
};
</script>

<div class="mt-12 border-t border-white/5 pt-8">
<div class="glass-card rounded-2xl p-6">
<div class="flex items-start gap-4">
<img
src="/sunithvs.png"
alt="Sunith VS"
class="h-14 w-14 shrink-0 rounded-full object-cover"
/>
<div>
<p class="text-sm text-gray-500">Written and verified by</p>
<a href={author.website} target="_blank" rel="noopener noreferrer" class="text-lg font-semibold text-white hover:text-brand-400 transition">
{author.name}
</a>
<p class="mt-1 text-sm text-gray-400 leading-relaxed">
Building tools that help Indian developers ship without ISP interference. Creator of <a href="/" class="text-brand-400 hover:underline">JioBase</a>.
</p>
<div class="mt-3 flex items-center gap-4">
<a href={author.website} target="_blank" rel="noopener noreferrer" class="text-gray-500 transition hover:text-white" aria-label="Website">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
</a>
<a href={author.linkedin} target="_blank" rel="noopener noreferrer" class="text-gray-500 transition hover:text-white" aria-label="LinkedIn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
<a href={author.instagram} target="_blank" rel="noopener noreferrer" class="text-gray-500 transition hover:text-white" aria-label="Instagram">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"/>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"/>
</svg>
</a>
<a href={author.youtube} target="_blank" rel="noopener noreferrer" class="text-gray-500 transition hover:text-white" aria-label="YouTube">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814z"/>
<polygon points="9.545 15.568 15.818 12 9.545 8.432" fill="#0a0a0a"/>
</svg>
</a>
<a href={author.twitter} target="_blank" rel="noopener noreferrer" class="text-gray-500 transition hover:text-white" aria-label="X (Twitter)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
184 changes: 184 additions & 0 deletions apps/web/src/lib/components/BlogSuggestions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<script lang="ts">
type BlogEntry = {
slug: string;
title: string;
description: string;
badge: string;
badgeColor: 'red' | 'brand' | 'amber' | 'blue' | 'purple';
readTime: string;
};

const ALL_BLOGS: BlogEntry[] = [
{
slug: 'supabase-blocked-india-fix',
title: 'Supabase Blocked in India: What Happened and How to Fix It',
description: 'Indian ISPs are DNS-blocking *.supabase.co. Here is everything you need to know and how to fix it.',
badge: 'Breaking',
badgeColor: 'red',
readTime: '8 min'
},
{
slug: 'proxy-supabase-cloudflare-workers',
title: 'How to Proxy Supabase Through Cloudflare Workers (Step-by-Step)',
description: 'A detailed tutorial on setting up a Cloudflare Worker reverse proxy for Supabase.',
badge: 'Tutorial',
badgeColor: 'brand',
readTime: '12 min'
},
{
slug: 'why-indian-developers-need-supabase-proxy',
title: 'Why Every Indian Developer Using Supabase Needs a Reverse Proxy',
description: 'ISP-level blocks keep breaking developer tools. Here is why a proxy layer is essential infrastructure.',
badge: 'Opinion',
badgeColor: 'amber',
readTime: '6 min'
},
{
slug: 'firebase-supabase-blocked-india',
title: 'Firebase AND Supabase Blocked in India: The Double Backend Crisis',
description: 'Firebase blocked on BSNL. Supabase blocked on Jio, Airtel, and ACT. Here is what happened.',
badge: 'Breaking',
badgeColor: 'red',
readTime: '7 min'
},
{
slug: 'test-if-backend-blocked-india',
title: 'How to Test if Your Backend is Blocked by Indian ISPs',
description: 'Step-by-step diagnostic guide to check if Supabase, Firebase, or any backend is being DNS-blocked.',
badge: 'Guide',
badgeColor: 'blue',
readTime: '5 min'
},
{
slug: 'supabase-alternatives-india',
title: 'Supabase Alternatives for Indian Developers (2026 Comparison)',
description: 'Evaluating Appwrite, Nhost, PocketBase, self-hosting, and custom backends as alternatives.',
badge: 'Comparison',
badgeColor: 'purple',
readTime: '8 min'
},
{
slug: 'fix-supabase-jio-5-minutes',
title: 'Fix Supabase on Jio in 5 Minutes',
description: 'Ultra-short, action-focused guide. No theory, no backstory - just the fix.',
badge: 'Quick Fix',
badgeColor: 'brand',
readTime: '3 min'
},
{
slug: 'supabase-err-connection-timed-out-india',
title: 'Supabase ERR_CONNECTION_TIMED_OUT in India: What It Means and How to Fix It',
description: 'Getting timeout errors with Supabase in India? Your ISP is blocking the connection.',
badge: 'Troubleshooting',
badgeColor: 'amber',
readTime: '5 min'
},
{
slug: 'why-supabase-banned-india-section-69a',
title: 'Why Is Supabase Banned in India? Section 69A Explained',
description: 'Understanding Section 69A of the IT Act and how blocking orders work.',
badge: 'Explainer',
badgeColor: 'purple',
readTime: '6 min'
},
{
slug: 'supabase-india-block-timeline',
title: 'Supabase India: Complete Timeline of the Block (Feb 2026)',
description: 'A day-by-day timeline of the Supabase block in India, from the government order to community workarounds.',
badge: 'Timeline',
badgeColor: 'blue',
readTime: '7 min'
},
{
slug: 'supabase-production-app-broken-india',
title: 'Your Production App Just Broke: Emergency Guide for Supabase Block in India',
description: 'Emergency triage guide with quick diagnosis, what NOT to do, and a 15-minute fix with code examples.',
badge: 'Emergency',
badgeColor: 'red',
readTime: '6 min'
},
{
slug: 'india-blocking-developer-tools-history',
title: "India's History of Blocking Developer Tools: From GitHub to Supabase",
description: 'From GitHub in 2014 to Supabase in 2026, the full history of ISP-level blocks affecting developers in India.',
badge: 'Deep Dive',
badgeColor: 'purple',
readTime: '8 min'
},
{
slug: 'dns-poisoning-supabase-india-explained',
title: 'DNS Poisoning Explained: How Indian ISPs Block Supabase',
description: 'A technical deep dive into DNS poisoning, sinkhole IPs, SNI inspection, and why a reverse proxy is the only reliable fix.',
badge: 'Technical',
badgeColor: 'blue',
readTime: '10 min'
},
{
slug: 'supabase-vs-firebase-both-blocked-india',
title: 'Supabase vs Firebase in India: Both Blocked, One Solution',
description: 'Both major BaaS platforms face ISP blocks in India. Compare the blocks, why switching does not help, and the one fix.',
badge: 'Comparison',
badgeColor: 'purple',
readTime: '7 min'
},
{
slug: 'india-disrupts-supabase-blocking-order',
title: 'India Disrupts Access to Supabase with Government Blocking Order',
description: 'A government blocking order under Section 69A has disrupted Supabase access across India. Full news coverage and analysis.',
badge: 'News',
badgeColor: 'blue',
readTime: '6 min'
},
{
slug: 'supabase-network-connectivity-problems-india',
title: 'Supabase Network Connectivity Problems in India: Causes, Diagnosis, and Fix',
description: 'Users experiencing network connectivity problems with Supabase in India? It is not a Supabase outage. Full diagnosis and fix guide.',
badge: 'Troubleshooting',
badgeColor: 'amber',
readTime: '7 min'
}
];

const BADGE_CLASSES: Record<string, string> = {
red: 'border-red-500/20 bg-red-500/5 text-red-400',
brand: 'border-brand-400/20 bg-brand-400/5 text-brand-400',
amber: 'border-amber-400/20 bg-amber-400/5 text-amber-400',
blue: 'border-blue-400/20 bg-blue-400/5 text-blue-400',
purple: 'border-purple-400/20 bg-purple-400/5 text-purple-400'
};

let { currentSlug = '', suggestedSlugs = [] as string[] }: { currentSlug?: string; suggestedSlugs?: string[] } = $props();

const suggestions = suggestedSlugs.length > 0
? ALL_BLOGS.filter((b) => suggestedSlugs.includes(b.slug) && b.slug !== currentSlug).slice(0, 3)
: ALL_BLOGS.filter((b) => b.slug !== currentSlug).slice(0, 3);
</script>

<div class="mt-12 border-t border-white/5 pt-8">
<h2 class="text-xl font-semibold text-white">Suggested reading</h2>
<p class="mt-1 text-sm text-gray-500">More guides on Supabase, DNS blocks, and building resilient apps in India.</p>

<div class="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each suggestions as post}
<a href="/blog/{post.slug}" class="group glass-card rounded-xl p-5 transition hover:border-white/10">
<div class="flex items-center gap-2">
<span class="rounded-full border px-2 py-0.5 text-xs font-medium {BADGE_CLASSES[post.badgeColor]}">
{post.badge}
</span>
<span class="text-xs text-gray-500">{post.readTime}</span>
</div>
<h3 class="mt-2 text-sm font-medium text-white group-hover:text-brand-400 transition leading-snug">
{post.title}
</h3>
<p class="mt-1 text-xs text-gray-500 leading-relaxed line-clamp-2">
{post.description}
</p>
</a>
{/each}
</div>

<div class="mt-6 flex items-center justify-between">
<a href="/blog" class="text-sm text-gray-400 hover:text-white transition">View all posts</a>
<a href="/" class="text-sm text-brand-400 hover:text-brand-300 transition">JioBase Home</a>
</div>
</div>
Loading