Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Supabase Auth example. #224

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This repository contains examples that use [Hono](https://hono.dev).
- [deno](./deno/) - Deno example
- [bun](./bun/) - Bun example
- [pages-stack](./pages-stack/) - Zod + Zod Validator + `hc` + React on Cloudflare Pages
- [Supabase](./supabase-auth/) - Example of using Supabase Auth and database on both server and client side (built on [hono-vite-jsx](./hono-vite-jsx/)).

## Running Examples

Expand Down
3 changes: 3 additions & 0 deletions supabase-auth/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Get your keys at https://supabase.com/dashboard/project/_/settings/api
VITE_SUPABASE_URL=your_supabase_url
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
1 change: 1 addition & 0 deletions supabase-auth/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
59 changes: 59 additions & 0 deletions supabase-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Hono Supabase Auth Example!

Based on the Hono/JSX + Vite example by [@MathurAditya724](https://github.com/MathurAditya724) \o/

This example shows how to use Supabase Auth both on the client and server side with Hono.

## Supabase setup

- Create a new Supabase project at [database.new](https://database.new/)
- Go to the `SQL Editor` and run the following query to create the `countries` table.

```sql
-- Create the table
create table countries (
id bigint primary key generated always as identity,
name text not null
);
-- Insert some sample data into the table
insert into countries (name)
values
('Canada'),
('United States'),
('Mexico');

alter table countries enable row level security;
```

- In a new query, create the following access policy.

```sql
create policy "public can read countries"
on public.countries
for select to authenticated
using (true);
```

- [Enable anonymous sign-ins](https://supabase.com/dashboard/project/_/settings/auth) in the Auth settings.

## Setup

- Run `npm install` to install the dependencies.
- Run `cp .env.example .env`.
- Set the required environment vairables in your `.env` file.

## Commands

Run the `vite` dev server

```bash
npm run dev
```

Building

```bash
npm run build
```

This project is configured to use `node` runtime, you can change it to your desired runtime in the `vite.config.js` file. We are using [@hono/vite-build](https://www.npmjs.com/package/@hono/vite-build) package for building the project and [@hono/vite-dev-server](https://www.npmjs.com/package/@hono/vite-dev-server) for running the dev server.
20 changes: 20 additions & 0 deletions supabase-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "hono-vite-jsx",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --mode client && vite build"
},
"dependencies": {
"@hono/node-server": "^1.12.2",
"@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.47.10",
"hono": "^4.5.10"
},
"devDependencies": {
"@hono/vite-build": "^1.1.0",
"@hono/vite-dev-server": "^0.17.0",
"@types/node": "^20.11.17",
"vite": "^5.4.2"
}
}
114 changes: 114 additions & 0 deletions supabase-auth/src/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { createBrowserClient } from '@supabase/ssr';
import { hc } from 'hono/client';
import { useEffect, useState } from 'hono/jsx';
import { render } from 'hono/jsx/dom';
import type { AppType } from '.';

const client = hc<AppType>('/');

const supabase = createBrowserClient(
import.meta.env.VITE_SUPABASE_URL!,
import.meta.env.VITE_SUPABASE_ANON_KEY!
);

function App() {
const [user, setUser] = useState<null | { id: string }>(null);
// Check client-side if user is logged in:
useEffect(() => {
supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth event:', event);
if (event === 'SIGNED_OUT') {
setUser(null);
} else {
setUser(session?.user!);
}
});
}, []);

return (
<>
<h1>Hono Supabase Auth Example!</h1>
<h2>Sign in</h2>
{!user ? (
<SignIn />
) : (
<button
type="button"
onClick={() => {
window.location.href = '/signout';
}}
>
Sign out!
</button>
)}
<h2>Example of API fetch()</h2>
<UserDetailsButton />
<h2>Example of database read</h2>
<p>
Note that only authenticated users are able to read from the database!
</p>
<a href="/countries">Get countries</a>
</>
);
}

function SignIn() {
return (
<>
<p>
Ready about and enable{' '}
<a
href="https://supabase.com/docs/guides/auth/auth-anonymous"
target="_blank"
>
anonymous signins here!
</a>
</p>
<button
type="button"
onClick={async () => {
const { data, error } = await supabase.auth.signInAnonymously();
if (error) return console.error('Error signing in:', error.message);
console.log('Signed in client-side!');
alert('Signed in anonymously! User id: ' + data?.user?.id);
}}
>
Anonymous sign in
</button>
</>
);
}

const UserDetailsButton = () => {
const [response, setResponse] = useState<string | null>(null);

const handleClick = async () => {
const response = await client.api.user.$get();
const data = await response.json();
const headers = Array.from(response.headers.entries()).reduce<
Record<string, string>
>((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
const fullResponse = {
url: response.url,
status: response.status,
headers,
body: data,
};
setResponse(JSON.stringify(fullResponse, null, 2));
};

return (
<div>
<button type="button" onClick={handleClick}>
Get My User Details
</button>
{response && <pre>{response}</pre>}
</div>
);
};

const root = document.getElementById('root')!;
render(<App />, root);
64 changes: 64 additions & 0 deletions supabase-auth/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Hono } from 'hono';
import { getSupabase, supabaseMiddleware } from './middleware/auth.middleware';

const app = new Hono();
app.use('*', supabaseMiddleware());

const routes = app.get('/api/user', async (c) => {
const supabase = getSupabase(c);
const { data, error } = await supabase.auth.getUser();

if (error) console.log('error', error);

if (!data?.user) {
return c.json({
message: 'You are not logged in.',
});
}

return c.json({
message: 'You are logged in!',
userId: data.user,
});
});

app.get('/signout', async (c) => {
const supabase = getSupabase(c);
await supabase.auth.signOut();
console.log('Signed out server-side!');
return c.redirect('/');
});

app.get('/countries', async (c) => {
const supabase = getSupabase(c);
const { data, error } = await supabase.from('countries').select('*');
if (error) console.log(error);
return c.json(data);
});

export type AppType = typeof routes;

app.get('/', (c) => {
return c.html(
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<link
rel="stylesheet"
href="https://cdn.simplecss.org/simple.min.css"
/>
{import.meta.env.PROD ? (
<script type="module" src="/static/client.js" />
) : (
<script type="module" src="/src/client.tsx" />
)}
</head>
<body>
<div id="root" />
</body>
</html>
);
});

export default app;
56 changes: 56 additions & 0 deletions supabase-auth/src/middleware/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createServerClient, parseCookieHeader } from '@supabase/ssr';
import { SupabaseClient } from '@supabase/supabase-js';
import type { Context, MiddlewareHandler } from 'hono';
import { env } from 'hono/adapter';
import { setCookie } from 'hono/cookie';

declare module 'hono' {
interface ContextVariableMap {
supabase: SupabaseClient;
}
}

export const getSupabase = (c: Context) => {
return c.get('supabase');
};

type SupabaseEnv = {
VITE_SUPABASE_URL: string;
VITE_SUPABASE_ANON_KEY: string;
};

export const supabaseMiddleware = (): MiddlewareHandler => {
return async (c, next) => {
const supabaseEnv = env<SupabaseEnv>(c);
const supabaseUrl =
supabaseEnv.VITE_SUPABASE_URL ?? import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey =
supabaseEnv.VITE_SUPABASE_ANON_KEY ??
import.meta.env.VITE_SUPABASE_ANON_KEY;

if (!supabaseUrl) {
throw new Error('SUPABASE_URL missing!');
}

if (!supabaseAnonKey) {
throw new Error('SUPABASE_ANON_KEY missing!');
}

const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
cookies: {
getAll() {
return parseCookieHeader(c.req.header('Cookie') ?? '');
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
setCookie(c, name, value, options)
);
},
},
});

c.set('supabase', supabase);

await next();
};
};
12 changes: 12 additions & 0 deletions supabase-auth/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["@cloudflare/workers-types", "vite/client"],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}
33 changes: 33 additions & 0 deletions supabase-auth/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import devServer from "@hono/vite-dev-server";
import { defineConfig } from "vite";

// Change the import to use your runtime specific build
import build from "@hono/vite-build/node";

export default defineConfig(({ mode }) => {
if (mode === "client")
return {
esbuild: {
jsxImportSource: "hono/jsx/dom", // Optimized for hono/jsx/dom
},
build: {
rollupOptions: {
input: "./src/client.tsx",
output: {
entryFileNames: "static/client.js",
},
},
},
};

return {
plugins: [
build({
entry: "src/index.tsx",
}),
devServer({
entry: "src/index.tsx",
}),
],
};
});