Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 56 additions & 0 deletions apps/web/src/lib/components/BlogSuggestions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,62 @@
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'
}
];

Expand Down
143 changes: 143 additions & 0 deletions apps/web/src/lib/components/DonationModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<script lang="ts">
type Variant = 'celebration' | 'periodic';

let {
open = $bindable(false),
variant = 'periodic' as Variant,
onclose = () => {},
}: { open: boolean; variant?: Variant; onclose?: () => void } = $props();

function dismiss() {
open = false;
onclose();
}

function handleBackdrop(e: MouseEvent) {
if (e.target === e.currentTarget) dismiss();
}

function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') dismiss();
}
Comment on lines +19 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The global Escape key handler calls the dismiss function unconditionally, so pressing Escape when the modal is not open still triggers the close callback, which can cause parent state to update as if the modal were closed even though it wasn't visible. Guard on the open flag before dismissing so the handler only runs when the modal is actually shown. [logic error]

Severity Level: Major ⚠️
- ⚠️ DonationModal onclose fires when modal is already closed.
- ⚠️ Parent logic relying on onclose may mis-trigger.
Suggested change
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') dismiss();
}
function handleKeydown(e: KeyboardEvent) {
if (open && e.key === 'Escape') dismiss();
}
Steps of Reproduction ✅
1. Mount `DonationModal` from `apps/web/src/lib/components/DonationModal.svelte` with
`open={false}` and a non‑no‑op `onclose` callback (lines 4–8 define these props).

2. When the component mounts, Svelte registers `<svelte:window onkeydown={handleKeydown}
/>` (lines 24–25), so `handleKeydown` is active regardless of `open`.

3. Ensure the modal is closed (`open === false` so the `{#if open}` block at lines 27–126
does not render any visible modal content).

4. Press the Escape key in the browser; `handleKeydown` (lines 19–21) runs, calls
`dismiss()` (lines 10–13), which invokes the parent's `onclose()` even though the modal
was not open, causing parent state or side effects to fire unexpectedly.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** apps/web/src/lib/components/DonationModal.svelte
**Line:** 19:21
**Comment:**
	*Logic Error: The global Escape key handler calls the dismiss function unconditionally, so pressing Escape when the modal is not open still triggers the close callback, which can cause parent state to update as if the modal were closed even though it wasn't visible. Guard on the `open` flag before dismissing so the handler only runs when the modal is actually shown.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

</script>

<svelte:window onkeydown={handleKeydown} />

{#if open}
<!-- Backdrop -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-fade-in"
onclick={handleBackdrop}
>
<!-- Modal -->
<div class="w-full max-w-md animate-scale-in">
<div class="glass-card rounded-2xl border border-white/10 p-6 sm:p-8 relative overflow-hidden">
<!-- Ambient glow -->
<div class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-amber-400/10 blur-3xl"></div>

<!-- Close button -->
<button
onclick={dismiss}
class="absolute top-4 right-4 text-gray-500 hover:text-white transition"
aria-label="Close"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>

{#if variant === 'celebration'}
<!-- Celebration variant -->
<div class="flex items-center gap-3 mb-4">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-brand-400/10">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
</div>
<h3 class="text-lg font-bold text-white">Your app is live!</h3>
</div>

<p class="text-sm text-gray-400 leading-relaxed">
Traffic is now routing through Cloudflare's edge network. Your users won't even notice a difference — except that everything works now.
</p>

<div class="mt-5 rounded-xl border border-amber-400/10 bg-amber-400/5 p-4">
<p class="text-sm text-gray-300 leading-relaxed">
Hey, I'm <span class="font-semibold text-white">Sunith</span> — I built JioBase because the Supabase block broke my own app and I knew other devs were stuck too. It's free because no one should pay to fix someone else's problem.
</p>
<p class="mt-3 text-sm text-gray-400 leading-relaxed">
But Cloudflare bills are real. If JioBase just saved your production app, <span class="text-amber-300 font-medium">a $3 coffee helps me keep the lights on</span> for everyone.
</p>
</div>
{:else}
<!-- Periodic variant -->
<div class="flex items-center gap-3 mb-4">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-400/10 text-2xl">
</div>
<h3 class="text-lg font-bold text-white">Quick reality check</h3>
</div>

<p class="text-sm text-gray-300 leading-relaxed">
JioBase proxies millions of requests for free. Every single request costs me money.
</p>

<p class="mt-3 text-sm text-gray-400 leading-relaxed">
I'm not a company. I'm <span class="font-semibold text-white">one developer</span> paying Cloudflare bills out of pocket so your Supabase app works in India.
</p>

<div class="mt-4 flex items-center gap-3 rounded-lg border border-white/5 bg-white/[0.03] px-4 py-3">
<span class="text-2xl">☕</span>
<p class="text-sm text-gray-300">
<span class="text-amber-300 font-semibold">$3</span> = one coffee = JioBase stays free for hundreds of devs
</p>
</div>

<p class="mt-4 text-xs text-gray-500 leading-relaxed">
No pressure — but it genuinely helps keep this project alive.
</p>
{/if}

<!-- Buttons -->
<div class="mt-6 flex flex-col gap-3 sm:flex-row">
<a
href="https://buymeacoffee.com/sunithvs"
target="_blank"
rel="noopener"
class="flex flex-1 items-center justify-center gap-2 rounded-xl bg-amber-400 px-5 py-3 text-sm font-semibold text-black transition hover:bg-amber-300 shadow-lg shadow-amber-400/20"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8h1a4 4 0 0 1 0 8h-1M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8zM6 1v3M10 1v3M14 1v3"/>
</svg>
{variant === 'celebration' ? 'Buy me a coffee' : 'Support JioBase'}
</a>
<button
onclick={dismiss}
class="rounded-xl border border-white/10 px-5 py-3 text-sm text-gray-400 transition hover:border-white/20 hover:text-white"
>
{variant === 'celebration' ? 'Maybe later' : 'Not now'}
</button>
</div>
</div>
</div>
</div>
{/if}

<style>
.animate-fade-in {
animation: fadeIn 0.2s ease-out;
}
.animate-scale-in {
animation: scaleIn 0.25s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
</style>
7 changes: 5 additions & 2 deletions apps/web/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import { getAuth } from '$lib/stores/auth.svelte.js';
import { onMount } from 'svelte';

Expand All @@ -13,7 +12,11 @@
</script>

<svelte:head>
<link rel="icon" href={favicon} />
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#3ecf8e" />
<title>JioBase</title>
</svelte:head>

Expand Down
Loading