Skip to content

Commit

Permalink
Merge pull request #2 from wataruoguchi/supabase-kysely
Browse files Browse the repository at this point in the history
supabase-kysely with postgresjs
wataruoguchi authored Nov 11, 2024
2 parents 608ca1e + 366e495 commit cc19baf
Showing 15 changed files with 970 additions and 52 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -6,3 +6,9 @@ node_modules
.dev.vars

.wrangler

# Worker
worker-configuration.d.ts

# Supabase
db/supabase.types.ts
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cSpell.words": ["Kysely", "SUPABASE"]
}
56 changes: 19 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,29 @@
# Welcome to Remix + Cloudflare!
# PoC - User

- 📖 [Remix docs](https://remix.run/docs)
- 📖 [Remix Cloudflare docs](https://remix.run/guides/vite#cloudflare)
## Key Frameworks and Libraries

## Development
- Hono
- Remix
- Kysely
- Supabase

Run the dev server:
## Steps

```sh
npm run dev
```

To run Wrangler:
1. [x] Integrate Hono and Remix by installing `hono-remix-adapter`
1. [ ] Integrate Supabase via Kysely

```sh
npm run build
npm run start
```
## Logs

## Typegen

Generate types for your Cloudflare bindings in `wrangler.toml`:
### Supabase x Kysely

```sh
npm run typegen
pnpx supabase login
pnpx supabase init
pnpx supabase gen types --lang=typescript --project-id <prject ref, like `abcdefghijklmnopqrst`> --schema public > db/supabase.types.ts
# At this point, the database has no user-defined tables.
pnpm run migrate -- create user # Run `kysely-migration-cli` internally.
pnpm run migrate -- up # It created the `user` table on Supabase!
pnpx supabase gen types --lang=typescript --project-id <prject ref, like `abcdefghijklmnopqrst`> --schema public > db/supabase.types.ts # Update the type declaration. We don't need to depend on `kysely-codegen`
```

You will need to rerun typegen whenever you make changes to `wrangler.toml`.

## Deployment

First, build your app for production:

```sh
npm run build
```

Then, deploy your app to Cloudflare Pages:

```sh
npm run deploy
```

## Styling

This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information.
- <https://supabase.com/docs/guides/deployment/managing-environments>
13 changes: 10 additions & 3 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/cloudflare";
import { useLoaderData } from "@remix-run/react";
import { WorkerDb } from "lib/db";

export const meta: MetaFunction = () => {
return [
@@ -8,14 +9,19 @@ export const meta: MetaFunction = () => {
];
};

export const loader = (args: LoaderFunctionArgs) => {
export const loader = async (args: LoaderFunctionArgs) => {
const extra = args.context.extra;
const cloudflare = args.context.cloudflare;
return { extra, cloudflare };
const db = await WorkerDb.getInstance(cloudflare.env);
const nOfUsers = await db
.selectFrom("person")
.select(({ fn }) => [fn.countAll<number>().as("count")])
.executeTakeFirst();
return { extra, cloudflare, nOfUsers };
};

export default function Index() {
const { cloudflare, extra } = useLoaderData<typeof loader>();
const { cloudflare, extra, nOfUsers } = useLoaderData<typeof loader>();

return (
<div className="flex h-screen items-center justify-center">
@@ -24,6 +30,7 @@ export default function Index() {
<h1 className="leading text-2xl font-bold text-gray-800 dark:text-gray-100">
Welcome to <span className="sr-only">Remix</span>
</h1>
<h2>Number of users: {nOfUsers ? nOfUsers.count : "unknown"}</h2>
<h2>Var is {cloudflare.env.MY_VAR}</h2>
<h3>
{cloudflare.cf ? "cf," : ""}
13 changes: 13 additions & 0 deletions db/migrations/20241111T015010-person.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Kysely } from "kysely";

export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable("person")
.addColumn("id", "uuid", (col) => col.primaryKey())
.execute();
}

export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable("person").execute();
}
33 changes: 33 additions & 0 deletions db/scripts/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FileMigrationProvider, Migrator } from "kysely";
import { run } from "kysely-migration-cli";
import { promises as fs } from "node:fs";
import * as path from "node:path";
import { WorkerDb } from "../../lib/db";

const SUPABASE_URI = process.env.SUPABASE_URI;
if (!SUPABASE_URI) {
throw new Error("SUPABASE_URI is not set");
}

// For ESM environment
const migrationFolder = path.resolve(
path.dirname(new URL(import.meta.url).pathname),
"../migrations"
);
const db = await WorkerDb.getInstance({
/**
* https://supabase.com/docs/guides/database/connecting-to-postgres#supavisor-session-mode-port-5432
*/
SUPABASE_URI: (SUPABASE_URI || "").replace(":6543", ":5432"),
});

const migrator = new Migrator({
db,
provider: new FileMigrationProvider({
fs,
path,
migrationFolder,
}),
});

run(db, migrator, migrationFolder);
33 changes: 33 additions & 0 deletions lib/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Supabase x Kysely.
* Supabase should appear only in this file.
*/
import { Kysely } from "kysely";
import { PostgresJSDialect } from "kysely-postgres-js";
import { type KyselifyDatabase } from "kysely-supabase";
import postgres from "postgres";
import { type Database } from "../db/supabase.types";

type DB = KyselifyDatabase<Database>;
export class WorkerDb {
private static instance: Kysely<DB> | null = null;

static async getInstance(
env: Pick<Env, "SUPABASE_URI">
): Promise<Kysely<DB>> {
if (!this.instance) {
const pg = postgres(env.SUPABASE_URI);
/**
* The following line is to check if the connection is successful.
*/
await pg.unsafe("SELECT 1");

this.instance = new Kysely<DB>({
dialect: new PostgresJSDialect({
postgres: pg,
}),
});
}
return this.instance;
}
}
6 changes: 1 addition & 5 deletions load-context.ts
Original file line number Diff line number Diff line change
@@ -3,10 +3,6 @@
import { type AppLoadContext } from "@remix-run/cloudflare";
import { type PlatformProxy } from "wrangler";

interface Env {
MY_VAR: string;
}

type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;

declare module "@remix-run/cloudflare" {
@@ -23,6 +19,6 @@ type GetLoadContext = (args: {
export const getLoadContext: GetLoadContext = ({ context }) => {
return {
...context,
extra: 'stuff'
extra: "stuff",
};
};
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -12,15 +12,22 @@
"typecheck": "tsc",
"typegen": "wrangler types",
"preview": "pnpm run build && wrangler pages dev",
"cf-typegen": "wrangler types"
"cf-typegen": "wrangler types",
"supabase:login": "pnpx supabase login",
"supabase:gen": "pnpx supabase gen types --lang=typescript --project-id $SUPABASE_PROJECT_REF --schema public > db/supabase.types.ts",
"migrate": "tsx --env-file=.dev.vars db/scripts/migrate.ts"
},
"dependencies": {
"@remix-run/cloudflare": "^2.13.1",
"@remix-run/cloudflare-pages": "^2.13.1",
"@remix-run/react": "^2.13.1",
"@supabase/supabase-js": "^2.46.1",
"hono": "^4.6.9",
"hono-remix-adapter": "^0.3.0",
"isbot": "^4.1.0",
"kysely": "^0.27.4",
"kysely-postgres-js": "^2.0.0",
"postgres": "^3.4.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -39,8 +46,12 @@
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"kysely-migration-cli": "^0.4.2",
"kysely-supabase": "^0.2.0",
"postcss": "^8.4.38",
"supabase": "^1.219.2",
"tailwindcss": "^3.4.4",
"tsx": "^4.19.2",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1",
561 changes: 561 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions supabase/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Supabase
.branches
.temp
.env
256 changes: 256 additions & 0 deletions supabase/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "poc-user-cf-hono-remix-kysely-supabase"

[api]
enabled = true
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` is always included.
schemas = ["public", "graphql_public"]
# Extra schemas to add to the search_path of every request. `public` is always included.
extra_search_path = ["public", "extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000

[api.tls]
enabled = false

[db]
# Port to use for the local database URL.
port = 54322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 15

[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
# How many server connections to allow per user/database pair.
default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100

[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory. For example:
# sql_paths = ['./seeds/*.sql', '../project-src/seeds/*-load-testing.sql']
sql_paths = ['./seed.sql']

[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096

[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"

# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[inbucket]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326

[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"

[storage.image_transformation]
enabled = true

# Uncomment to configure local storage buckets
# [storage.buckets.images]
# public = false
# file_size_limit = "50MiB"
# allowed_mime_types = ["image/png", "image/jpeg"]
# objects_path = "./images"

[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
# Requires enable_refresh_token_rotation = true.
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow anonymous sign-ins to your project.
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false

[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
# Number of characters used in the email OTP.
otp_length = 6
# Number of seconds before the email OTP expires (defaults to 1 hour).
otp_expiry = 3600

# Use a production-ready SMTP server
# [auth.email.smtp]
# host = "smtp.sendgrid.net"
# port = 587
# user = "apikey"
# pass = "env(SENDGRID_API_KEY)"
# admin_email = "admin@email.com"
# sender_name = "Admin"

# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"

[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = false
# If enabled, users need to confirm their phone number before signing in.
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ .Code }} ."
# Controls the minimum amount of time that must pass before sending another sms otp.
max_frequency = "5s"

# Use pre-defined map of phone number to OTP for testing.
# [auth.sms.test_otp]
# 4152127777 = "123456"

# Configure logged in session timeouts.
# [auth.sessions]
# Force log out after the specified duration.
# timebox = "24h"
# Force log out if the user has been inactive longer than the specified duration.
# inactivity_timeout = "8h"

# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"

# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"

[auth.mfa]
# Control how many MFA factors can be enrolled at once per user.
max_enrolled_factors = 10

# Control use of MFA via App Authenticator (TOTP)
[auth.mfa.totp]
enroll_enabled = true
verify_enabled = true

# Configure Multi-factor-authentication via Phone Messaging
# [auth.mfa.phone]
# enroll_enabled = true
# verify_enabled = true
# otp_length = 6
# template = "Your code is {{ .Code }} ."
# max_frequency = "10s"

# Configure Multi-factor-authentication via WebAuthn
# [auth.mfa.web_authn]
# enroll_enabled = true
# verify_enabled = true

# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
# Overrides the default auth redirectUrl.
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false

# Use Firebase Auth as a third-party provider alongside Supabase Auth.
[auth.third_party.firebase]
enabled = false
# project_id = "my-firebase-project"

# Use Auth0 as a third-party provider alongside Supabase Auth.
[auth.third_party.auth0]
enabled = false
# tenant = "my-auth0-tenant"
# tenant_region = "us"

# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
[auth.third_party.aws_cognito]
enabled = false
# user_pool_id = "my-user-pool-id"
# user_pool_region = "us-east-1"

[edge_runtime]
enabled = true
# Configure one of the supported request policies: `oneshot`, `per_worker`.
# Use `oneshot` for hot reload, or `per_worker` for load testing.
policy = "oneshot"
inspector_port = 8083

[analytics]
enabled = true
port = 54327
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"

# Experimental features may be deprecated any time
[experimental]
# Configures Postgres storage engine to use OrioleDB (S3)
orioledb_version = ""
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
s3_host = "env(S3_HOST)"
# Configures S3 bucket region, eg. us-east-1
s3_region = "env(S3_REGION)"
# Configures AWS_ACCESS_KEY_ID for S3 bucket
s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"
20 changes: 18 additions & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import {
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import serverAdapter from "hono-remix-adapter/vite";
import path from "node:path";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { getLoadContext } from "./load-context";
@@ -14,7 +15,7 @@ declare module "@remix-run/cloudflare" {
}
}

export default defineConfig({
export default defineConfig(({ mode }) => ({
plugins: [
remixCloudflareDevProxy(),
remix({
@@ -33,4 +34,19 @@ export default defineConfig({
}),
tsconfigPaths(),
],
});
/**
* In development, we need to use Node.js's postgres package instead of the cloudflare one.
* https://github.com/remix-run/remix/issues/9245#issuecomment-2179517678
* https://vite.dev/config/ssr-options.html#ssr-noexternal
*/
ssr: {
noExternal: ["postgres"],
},
resolve: {
alias: {
...(mode === "development" && {
postgres: path.resolve(__dirname, "node_modules/postgres/src/index.js"),
}),
},
},
}));
4 changes: 0 additions & 4 deletions worker-configuration.d.ts

This file was deleted.

1 change: 1 addition & 0 deletions wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#:schema node_modules/wrangler/config-schema.json
name = "poc-user-cf-hono-remix-kysely-supabase"
compatibility_date = "2024-11-06"
compatibility_flags = ["nodejs_compat_v2"]
pages_build_output_dir = "./build/client"

# Automatically place your workloads in an optimal location to minimize latency.

0 comments on commit cc19baf

Please sign in to comment.