├── .gitignore
├── .idea
├── .gitignore
├── hono-boilerplate.iml
├── modules.xml
└── vcs.xml
├── LICENSE
├── README.md
├── bun.lockb
├── index.ts
├── package.json
├── src
├── controllers
│ ├── AuthController.ts
│ └── CountryController.ts
├── db
│ └── supabaseClient.ts
├── helpers.ts
├── middlewares
│ └── authMiddleware.ts
└── routes.ts
├── tsconfig.json
└── wrangler.toml
/.gitignore:
--------------------------------------------------------------------------------
1 | # prod
2 | dist/
3 |
4 | # dev
5 | .yarn/
6 | !.yarn/releases
7 | .vscode/*
8 | !.vscode/launch.json
9 | !.vscode/*.code-snippets
10 | .idea/
11 | .idea/*
12 | # deps
13 | node_modules/
14 | .wrangler
15 | wrangler
16 |
17 | # env
18 | .env
19 | .env.production
20 | .dev.vars
21 | wrangler.toml
22 | # logs
23 | logs/
24 | *.log
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 | pnpm-debug.log*
29 | lerna-debug.log*
30 |
31 | # misc
32 | .DS_Store
33 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/hono-boilerplate.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Göktuğ Ceyhan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🔥 Hono + ⚡️ Supabase Boilerplate
2 |
3 | This project is a backend boilerplate built using the Hono framework. Cloudfare Workers are used to host the backend. It includes integration with Supabase and handles user authentication and authorization.
4 |
5 | [](https://github.com/prettier/prettier)
6 | [](https://github.com/goktugcy/hono-boilerplate/blob/main/LICENSE)
7 | 
8 | 
9 | 
10 |
11 | ## Installation 🚀
12 |
13 | After cloning the repository, install the necessary dependencies by running:
14 |
15 | ```sh
16 | bun install
17 | ```
18 |
19 | ## Configuration ✨
20 |
21 | Edit the `.wrangler.toml` file to include the necessary environment variables:
22 |
23 | ```sh
24 | STAGE = "dev" # dev or prod
25 | # Supabase
26 | SUPABASE_URL=
27 | SUPABASE_SERVICE_KEY=
28 | SUPABASE_ANON_KEY=
29 |
30 | ```
31 |
32 | ## Structure 🎄
33 |
34 | ```
35 | .
36 | ├── src
37 | │ ├── controllers
38 | │ │ ├── AuthController.ts
39 | │ │ ├── CountryController.ts
40 | │ ├── middlewares
41 | │ │ ├── authMiddleware.ts
42 | │ ├── db
43 | │ │ ├── supabaseClient.ts
44 | │ ├── routes.ts
45 | │ ├── helpers.ts
46 | ├── index.ts
47 | ├── wrangler.toml
48 | ├── package.json
49 | ├── README.md
50 | ├── tsconfig.json
51 | ├── .gitignore
52 | ├── bun.lockb
53 |
54 | ```
55 |
56 | ## Usage 🍻
57 |
58 | To start the server, run:
59 |
60 | ```sh
61 | bun dev
62 | ```
63 |
64 | Deploy the server to Cloudfare Workers by running:
65 |
66 | ```sh
67 | bun run deploy
68 | ```
69 |
70 | ## Authentication 🛡
71 |
72 | The boilerplate includes an authentication middleware that checks if the user is authenticated. The middleware is used in the routes.ts file to protect routes that require authentication.
73 |
74 | ```ts
75 | import { authMiddleware } from './middlewares/authMiddleware';
76 |
77 | router.get('/countries', authMiddleware, CountryController.index);
78 | ```
79 | #### Token Extraction: The middleware extracts the accessToken and refreshToken from the request cookies.
80 |
81 | #### Token Verification:
82 | * If the accessToken is expired but the refreshToken is valid, it requests a new accessToken using the refreshToken.
83 | * If both tokens are missing, it responds with an error.
84 | * If the accessToken is present, it verifies the token using Supabase's authentication service.
85 |
86 | ## Supabase Client Usage ⚡️
87 |
88 | The project uses two Supabase clients: `supabaseAnon` and `supabaseService`. These clients are created in the [`getSupabaseClient`](src/db/supabaseClient.ts) function in [src/db/supabaseClient.ts](src/db/supabaseClient.ts).
89 |
90 | ### `supabaseAnon`
91 |
92 | The `supabaseAnon` client is used for operations that do not require elevated privileges. It is created using the anonymous key (`SUPABASE_ANON_KEY`) and is configured to not automatically refresh tokens, persist sessions, or detect sessions in URLs.
93 |
94 | Example usage:
95 | ```ts
96 | import { getSupabaseClient } from "./src/db/supabaseClient";
97 |
98 | const getCountries = async (c: Context) => {
99 | const { supabaseAnon } = getSupabaseClient(c);
100 | let { data: countries, error } = await supabaseAnon.from("countries").select("*");
101 |
102 | if (error) {
103 | return c.json({ error: error.message }, 400);
104 | }
105 |
106 | return c.json(countries, 200);
107 | };
108 | ```
109 |
110 | ### `supabaseService`
111 |
112 | The `supabaseService` client is used for operations that require elevated privileges, such as authentication and user management. It is created using the service key (`SUPABASE_SERVICE_KEY`).
113 |
114 | Example usage:
115 | ```ts
116 | import { getSupabaseClient } from "./src/db/supabaseClient";
117 |
118 | const login = async (c: Context) => {
119 | const { supabaseService } = getSupabaseClient(c);
120 | const { email, password } = await c.req.json<{ email: string; password: string }>();
121 |
122 | const { data, error } = await supabaseService.auth.signInWithPassword({
123 | email,
124 | password,
125 | });
126 |
127 | if (error) {
128 | return c.json({ error: error.message }, 400);
129 | }
130 |
131 | const accessToken = data.session?.access_token;
132 | const refreshToken = data.session?.refresh_token;
133 |
134 | if (!accessToken || !refreshToken) {
135 | return c.json({ error: "Token creation failed" }, 500);
136 | }
137 |
138 | setAuthCookies(c, accessToken, refreshToken);
139 |
140 | return c.json({ message: "Login successful", accessToken, refreshToken }, 200);
141 | };
142 | ```
143 |
144 | ## License
145 |
146 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goktugcy/hono-boilerplate/09e9d8775aab02aafe60bf5f7c0805d2ab906acd/bun.lockb
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | import { Hono } from "hono";
2 | import { logger } from "hono/logger";
3 | import { prettyJSON } from "hono/pretty-json";
4 | import { cors } from "hono/cors";
5 | import routes from "./src/routes";
6 |
7 | // Initialize Hono app
8 | const app = new Hono();
9 |
10 | // Cors Middleware
11 | app.use(
12 | "*",
13 | cors({
14 | origin: "*",
15 | allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
16 | })
17 | );
18 |
19 | // Initialize middlewares
20 | app.use("*", logger(), prettyJSON());
21 |
22 | // default route
23 | app.get("/", async (c) => {
24 | return c.json({ message: "Welcome to Hono" }, 200);
25 | });
26 |
27 | // Initialize routes
28 | app.route("/", routes);
29 |
30 | export default app;
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hono-boilerplate",
3 | "scripts": {
4 | "dev": "wrangler dev",
5 | "deploy": "wrangler deploy --minify"
6 | },
7 | "dependencies": {
8 | "@supabase/supabase-js": "^2.49.1",
9 | "hono": "^4.7.2"
10 | },
11 | "devDependencies": {
12 | "@cloudflare/workers-types": "^4.20250224.0",
13 | "wrangler": "^3.109.3"
14 | },
15 | "packageManager": "pnpm@9.10.0+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c"
16 | }
17 |
--------------------------------------------------------------------------------
/src/controllers/AuthController.ts:
--------------------------------------------------------------------------------
1 | import { Context } from "hono";
2 | import { getSupabaseClient } from "../db/supabaseClient";
3 | import { deleteCookie } from "hono/cookie";
4 | import { setAuthCookies } from "../helpers";
5 |
6 | const login = async (c: Context) => {
7 | const { supabaseService } = getSupabaseClient(c);
8 | const { email, password } = await c.req.json<{
9 | email: string;
10 | password: string;
11 | }>();
12 |
13 | const { data, error } = await supabaseService.auth.signInWithPassword({
14 | email,
15 | password,
16 | });
17 |
18 | if (error) {
19 | return c.json({ error: error.message }, 400);
20 | }
21 |
22 | const accessToken = data.session?.access_token;
23 | const refreshToken = data.session?.refresh_token;
24 |
25 | if (!accessToken || !refreshToken) {
26 | return c.json({ error: "Token creation failed 😗" }, 500);
27 | }
28 |
29 | setAuthCookies(c, accessToken, refreshToken);
30 |
31 | return c.json(
32 | { message: "Login successful ⚡️", accessToken, refreshToken },
33 | 200
34 | );
35 | };
36 |
37 | const register = async (c: Context) => {
38 | const { supabaseAnon } = getSupabaseClient(c);
39 | const { email, password } = await c.req.json<{
40 | email: string;
41 | password: string;
42 | }>();
43 |
44 | const { data, error } = await supabaseAnon.auth.signUp({
45 | email: email,
46 | password: password,
47 | });
48 |
49 | if (error) {
50 | return c.json({ error: error.message }, 400);
51 | }
52 |
53 | return c.json({ message: "User registered successfully 🍻" }, 201);
54 | };
55 |
56 | const logout = async (c: Context) => {
57 | await getSupabaseClient(c).supabaseAnon.auth.signOut();
58 | deleteCookie(c, "accessToken");
59 | deleteCookie(c, "refreshToken");
60 | return c.json({ message: "Logout successful 🍻" }, 200);
61 | };
62 |
63 | export { login, register, logout };
64 |
--------------------------------------------------------------------------------
/src/controllers/CountryController.ts:
--------------------------------------------------------------------------------
1 | import { Context } from "hono";
2 | import { getSupabaseClient } from "../db/supabaseClient";
3 |
4 | const getCountries = async (c: Context) => {
5 | let { data: countries, error } = await getSupabaseClient(c)
6 | .supabaseAnon.from("countries")
7 | .select("id, name, iso2, iso3, local_name, continent");
8 |
9 | if (error) {
10 | return c.json({ error: error.message }, 400);
11 | }
12 |
13 | return c.json(countries, 200);
14 | };
15 |
16 | const getCountry = async (c: Context) => {
17 | const { id } = c.req.param();
18 | let { data: country, error } = await getSupabaseClient(c)
19 | .supabaseAnon.from("countries")
20 | .select("id, name, iso2, iso3, local_name, continent")
21 | .eq("id", id)
22 | .single();
23 |
24 | if (error) {
25 | return c.json({ error: error.message }, 400);
26 | }
27 |
28 | return c.json(country, 200);
29 | };
30 |
31 | export { getCountries, getCountry };
32 |
--------------------------------------------------------------------------------
/src/db/supabaseClient.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "@supabase/supabase-js";
2 | import { Context } from "hono";
3 |
4 | const getSupabaseClient = (c: Context) => {
5 | const supabaseUrl = c.env.SUPABASE_URL as string;
6 | const supabaseKey = c.env.SUPABASE_ANON_KEY as string;
7 | const serviceKey = c.env.SUPABASE_SERVICE_KEY as string;
8 |
9 | const supabaseService = createClient(supabaseUrl, serviceKey);
10 |
11 | const supabaseAnon = createClient(supabaseUrl, supabaseKey, {
12 | auth: {
13 | autoRefreshToken: false,
14 | persistSession: false,
15 | detectSessionInUrl: false,
16 | },
17 | });
18 |
19 | return { supabaseAnon, supabaseService };
20 | };
21 |
22 | export type SupabaseClients = ReturnType;
23 |
24 | export { getSupabaseClient };
25 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import { Context } from "hono";
2 | import { setCookie } from "hono/cookie";
3 |
4 | export const setAuthCookies = (
5 | c: Context,
6 | accessToken: string,
7 | refreshToken: string
8 | ) => {
9 | setCookie(c, "accessToken", accessToken, {
10 | httpOnly: true,
11 | secure: c.env.STAGE === "production",
12 | sameSite: "Strict",
13 | maxAge: 60 * 60, // 1 hour
14 | });
15 |
16 | setCookie(c, "refreshToken", refreshToken, {
17 | httpOnly: true,
18 | secure: c.env.STAGE === "production",
19 | sameSite: "Strict",
20 | maxAge: 60 * 60 * 24 * 30, // 30 days
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/src/middlewares/authMiddleware.ts:
--------------------------------------------------------------------------------
1 | import { Context, Next } from "hono";
2 | import { getSupabaseClient } from "../db/supabaseClient";
3 | import { getCookie } from "hono/cookie";
4 | import { setAuthCookies } from "../helpers";
5 |
6 | export const authMiddleware = async (c: Context, next: Next) => {
7 | const { supabaseAnon, supabaseService } = getSupabaseClient(c);
8 |
9 | const accessToken = getCookie(c, "accessToken");
10 | const refreshToken = getCookie(c, "refreshToken");
11 |
12 | if (!accessToken && refreshToken) {
13 | // A new access token is requested using the refresh token
14 | const { data, error } = await supabaseService.auth.refreshSession({
15 | refresh_token: refreshToken,
16 | });
17 |
18 | if (error || !data.session) {
19 | return c.json({ error: "Session refresh failed" }, 401);
20 | }
21 |
22 | // Set the new access token and refresh token
23 | setAuthCookies(c, data.session.access_token, data.session.refresh_token);
24 |
25 | c.set("user", data.user);
26 | } else if (!accessToken || !refreshToken) {
27 | return c.json({ error: "Authorization tokens missing" }, 401);
28 | } else {
29 | const { data, error } = await supabaseAnon.auth.getUser(accessToken);
30 |
31 | if (error || !data.user) {
32 | return c.json({ error: "Unauthorized" }, 401);
33 | }
34 |
35 | c.set("user", data.user);
36 | }
37 |
38 | await next();
39 | };
40 |
--------------------------------------------------------------------------------
/src/routes.ts:
--------------------------------------------------------------------------------
1 | import { Hono } from "hono";
2 | import { login, register, logout } from "./controllers/AuthController";
3 | import { authMiddleware } from "./middlewares/authMiddleware";
4 | import { getCountries, getCountry } from "./controllers/CountryController";
5 | const routes = new Hono().basePath("/api/v1");
6 | // Auth routes
7 | routes.post("/login", login);
8 | routes.post("/register", register);
9 | routes.post("/logout", logout);
10 |
11 | // Protected route with authentication
12 | routes.get("/countries", authMiddleware, getCountries);
13 | routes.get("/countries/:id", authMiddleware, getCountry);
14 |
15 | routes.get("/protected", authMiddleware, async (c) => {
16 | return c.json({ message: "This is a protected route" }, 200);
17 | });
18 |
19 | export default routes;
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "strict": true,
7 | "skipLibCheck": true,
8 | "lib": [
9 | "ESNext"
10 | ],
11 | "types": [
12 | "@cloudflare/workers-types/2023-07-01"
13 | ],
14 | "jsx": "react-jsx",
15 | "jsxImportSource": "hono/jsx"
16 | },
17 | }
--------------------------------------------------------------------------------
/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "hono-boilerplate"
2 | main = "./index.ts"
3 | compatibility_date = "2024-11-24"
4 |
5 | # compatibility_flags = [ "nodejs_compat" ]
6 |
7 |
8 | [vars]
9 | STAGE = "dev"
10 | SUPABASE_URL = "YOUR_SUPABASE_URL"
11 | SUPABASE_SERVICE_KEY = "YOUR_SUPABASE_SERVICE_KEY"
12 | SUPABASE_ANON_KEY = "YOUR_SUPABASE_ANON_KEY"
13 |
14 | # [[kv_namespaces]]
15 | # binding = "MY_KV_NAMESPACE"
16 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
17 |
18 | # [[r2_buckets]]
19 | # binding = "MY_BUCKET"
20 | # bucket_name = "my-bucket"
21 |
22 | # [[d1_databases]]
23 | # binding = "DB"
24 | # database_name = "hono-boilerplate"
25 | # database_id = "cb25a0d9-0eaa-49b0-9cde-beff6b904e7f"
26 |
27 | # [ai]
28 | # binding = "AI"
29 |
30 | # [observability]
31 | # enabled = true
32 | # head_sampling_rate = 1
--------------------------------------------------------------------------------