mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-15 20:13:50 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06fc2c2cd2 | |||
| b521314cc6 | |||
| 3fa3ace4ee | |||
| 67fcf8ea38 | |||
| 37a8062c75 | |||
| c32d62d420 | |||
| 1f0ff5bc5c | |||
| 103ee16cec | |||
| 337bc648ca | |||
| 14992c3798 | |||
| 48aaee744f | |||
| 2fe426b3f4 | |||
| a10d22d1f8 | |||
| 8634e56bd1 | |||
| d30d6181cf | |||
| 540352657c | |||
| f10e0537bd | |||
| 113268614b | |||
| e15565bb56 | |||
| fd4befcd12 | |||
| ed4b5f3725 | |||
| 83c8e44f33 | |||
| 66698445f2 | |||
| 1303c97f72 | |||
| 822b8c7235 |
@@ -8,5 +8,4 @@ node_modules
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
/static/output.css
|
||||
.netlify
|
||||
|
||||
@@ -41,4 +41,10 @@ In its current configuration, the application can be easily linked to Netlify vi
|
||||
|
||||
## To Do
|
||||
|
||||
- [Update types automatically](https://supabase.com/docs/guides/api/rest/generating-types#update-types-automatically-with-github-actions)
|
||||
|
||||
Please see the Issues tab for `enhancement` tagged issues.
|
||||
|
||||
## [Database setup](/docs/database-setup.md)
|
||||
|
||||
## [Stripe webhook event listening](/docs/stripe-setup.md)
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
# Database Setup
|
||||
|
||||
Databases are stored in Supabase. The auth table for signing up and logging in users is created automatically when you set up Supabase Auth.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
To add the tables for recording their active subscriptions and bought products, as well as products and prices, you can use the following SQL script in the Supabase SQL Editor.
|
||||
|
||||
```sql
|
||||
/**
|
||||
* USERS
|
||||
* Note: This table contains user data. Users should only be able to view and update their own data.
|
||||
*/
|
||||
create table users (
|
||||
-- UUID from auth.users
|
||||
id uuid references auth.users not null primary key,
|
||||
full_name text,
|
||||
avatar_url text,
|
||||
-- The customer's billing address, stored in JSON format.
|
||||
billing_address jsonb,
|
||||
-- Stores your customer's payment instruments.
|
||||
payment_method jsonb
|
||||
);
|
||||
alter table users
|
||||
enable row level security;
|
||||
create policy "Can view own user data." on users
|
||||
for select using ((select auth.uid()) = id);
|
||||
create policy "Can update own user data." on users
|
||||
for update using ((select auth.uid()) = id);
|
||||
|
||||
/**
|
||||
* This trigger automatically creates a user entry when a new user signs up via Supabase Auth.
|
||||
*/
|
||||
create function public.handle_new_user()
|
||||
returns trigger as
|
||||
$$
|
||||
begin
|
||||
insert into public.users (id, full_name, avatar_url)
|
||||
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
|
||||
return new;
|
||||
end;
|
||||
$$
|
||||
language plpgsql security definer;
|
||||
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row
|
||||
execute procedure public.handle_new_user();
|
||||
|
||||
/**
|
||||
* CUSTOMERS
|
||||
* Note: this is a private table that contains a mapping of user IDs to Stripe customer IDs.
|
||||
*/
|
||||
create table customers (
|
||||
-- UUID from auth.users
|
||||
id uuid references auth.users not null primary key,
|
||||
-- The user's customer ID in Stripe. User must not be able to update this.
|
||||
stripe_customer_id text
|
||||
);
|
||||
alter table customers enable row level security;
|
||||
-- No policies as this is a private table that the user must not have access to.
|
||||
|
||||
/**
|
||||
* PRODUCTS
|
||||
* Note: products are created and managed in Stripe and synced to our DB via Stripe webhooks.
|
||||
*/
|
||||
create table products (
|
||||
-- Product ID from Stripe, e.g. prod_1234.
|
||||
id text primary key,
|
||||
-- Whether the product is currently available for purchase.
|
||||
active boolean,
|
||||
-- The product's name, meant to be displayable to the customer. Whenever this product is sold via a subscription, name will show up on associated invoice line item descriptions.
|
||||
name text,
|
||||
-- The product's description, meant to be displayable to the customer. Use this field to optionally store a long form explanation of the product being sold for your own rendering purposes.
|
||||
description text,
|
||||
-- A URL of the product image in Stripe, meant to be displayable to the customer.
|
||||
image text,
|
||||
-- Set of key-value pairs, used to store additional information about the object in a structured format.
|
||||
metadata jsonb
|
||||
);
|
||||
alter table products
|
||||
enable row level security;
|
||||
create policy "Allow public read-only access." on products
|
||||
for select using (true);
|
||||
|
||||
/**
|
||||
* PRICES
|
||||
* Note: prices are created and managed in Stripe and synced to our DB via Stripe webhooks.
|
||||
*/
|
||||
create type pricing_type as enum ('one_time', 'recurring');
|
||||
create type pricing_plan_interval as enum ('day', 'week', 'month', 'year');
|
||||
create table prices (
|
||||
-- Price ID from Stripe, e.g. price_1234.
|
||||
id text primary key,
|
||||
-- The ID of the prduct that this price belongs to.
|
||||
product_id text references products,
|
||||
-- Whether the price can be used for new purchases.
|
||||
active boolean,
|
||||
-- A brief description of the price.
|
||||
description text,
|
||||
-- The unit amount as a positive integer in the smallest currency unit (e.g., 100 cents for US$1.00 or 100 for ¥100, a zero-decimal currency).
|
||||
unit_amount bigint,
|
||||
-- Three-letter ISO currency code, in lowercase.
|
||||
currency text check (char_length(currency) = 3),
|
||||
-- One of `one_time` or `recurring` depending on whether the price is for a one-time purchase or a recurring (subscription) purchase.
|
||||
type pricing_type,
|
||||
-- The frequency at which a subscription is billed. One of `day`, `week`, `month` or `year`.
|
||||
interval pricing_plan_interval,
|
||||
-- The number of intervals (specified in the `interval` attribute) between subscription billings. For example, `interval=month` and `interval_count=3` bills every 3 months.
|
||||
interval_count integer,
|
||||
-- Default number of trial days when subscribing a customer to this price using [`trial_from_plan=true`](https://stripe.com/docs/api#create_subscription-trial_from_plan).
|
||||
trial_period_days integer,
|
||||
-- Set of key-value pairs, used to store additional information about the object in a structured format.
|
||||
metadata jsonb
|
||||
);
|
||||
alter table prices
|
||||
enable row level security;
|
||||
create policy "Allow public read-only access." on prices
|
||||
for select using (true);
|
||||
|
||||
/**
|
||||
* SUBSCRIPTIONS
|
||||
* Note: subscriptions are created and managed in Stripe and synced to our DB via Stripe webhooks.
|
||||
*/
|
||||
create type subscription_status as enum ('trialing', 'active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid');
|
||||
create table subscriptions (
|
||||
-- Subscription ID from Stripe, e.g. sub_1234.
|
||||
id text primary key,
|
||||
user_id uuid references auth.users not null,
|
||||
-- The status of the subscription object, one of subscription_status type above.
|
||||
status subscription_status,
|
||||
-- Set of key-value pairs, used to store additional information about the object in a structured format.
|
||||
metadata jsonb,
|
||||
-- ID of the price that created this subscription.
|
||||
price_id text references prices,
|
||||
-- Quantity multiplied by the unit amount of the price creates the amount of the subscription. Can be used to charge multiple seats.
|
||||
quantity integer,
|
||||
-- If true the subscription has been canceled by the user and will be deleted at the end of the billing period.
|
||||
cancel_at_period_end boolean,
|
||||
-- Time at which the subscription was created.
|
||||
created timestamp with time zone default timezone('utc'::text, now()) not null,
|
||||
-- Start of the current period that the subscription has been invoiced for.
|
||||
current_period_start timestamp with time zone default timezone('utc'::text, now()) not null,
|
||||
-- End of the current period that the subscription has been invoiced for. At the end of this period, a new invoice will be created.
|
||||
current_period_end timestamp with time zone default timezone('utc'::text, now()) not null,
|
||||
-- If the subscription has ended, the timestamp of the date the subscription ended.
|
||||
ended_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
-- A date in the future at which the subscription will automatically get canceled.
|
||||
cancel_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
-- If the subscription has been canceled, the date of that cancellation. If the subscription was canceled with `cancel_at_period_end`, `canceled_at` will still reflect the date of the initial cancellation request, not the end of the subscription period when the subscription is automatically moved to a canceled state.
|
||||
canceled_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
-- If the subscription has a trial, the beginning of that trial.
|
||||
trial_start timestamp with time zone default timezone('utc'::text, now()),
|
||||
-- If the subscription has a trial, the end of that trial.
|
||||
trial_end timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
alter table subscriptions
|
||||
enable row level security;
|
||||
create policy "Can only view own subs data." on subscriptions
|
||||
for select using ((select auth.uid()) = user_id);
|
||||
|
||||
/**
|
||||
* REALTIME SUBSCRIPTIONS
|
||||
* Only allow realtime listening on public tables.
|
||||
*/
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime
|
||||
for table products, prices;
|
||||
```
|
||||
|
||||
## Generating Types
|
||||
|
||||
To get the types for the tables, you can follow these instructions taken from [here](https://supabase.com/docs/guides/api/rest/generating-types#generating-types-using-supabase-cli):
|
||||
|
||||
`npm i supabase@">=1.8.1" --save-dev`
|
||||
|
||||
`npx supabase login`
|
||||
|
||||
`npx supabase init`
|
||||
|
||||
Replace `$PROJECT_REF` with your project reference:
|
||||
`npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts`
|
||||
|
||||
Replace `types/supabase.d.ts` with whatever file you want your types in:
|
||||
`npx supabase gen types typescript --local > types/supabase.d.ts`
|
||||
|
||||
## Local DB Development
|
||||
|
||||
Ensure you have [Docker](https://docs.docker.com/get-docker/) installed and running.
|
||||
|
||||
You can start and stop the local DB environment with:
|
||||
|
||||
```bash
|
||||
npx supabase start
|
||||
|
||||
npx supabase stop
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
There are two options to create a migration file:
|
||||
|
||||
1. [Writing the SQL manually](https://supabase.com/docs/guides/cli/local-development#database-migrations)
|
||||
|
||||
- Create new blank migration file
|
||||
|
||||
`npx supabase migration new <migration_name>`
|
||||
|
||||
- Add the SQL you want to deploy as part of the migration e.g.
|
||||
|
||||
```sql
|
||||
create table
|
||||
employees (
|
||||
id bigint primary key generated always as identity,
|
||||
name text,
|
||||
email text,
|
||||
created_at timestamptz default now()
|
||||
);
|
||||
```
|
||||
|
||||
- Reset the DB and apply the latest migrations
|
||||
|
||||
`npx supabase db reset`
|
||||
|
||||
2. [Generate SQL based on differences to the schema](https://supabase.com/docs/guides/cli/local-development#diffing-changes)
|
||||
|
||||
- With the local DB running, enter the studio and make your changes to the schema
|
||||
|
||||
- Generate the migration file containing the SQL with the differences in the schema
|
||||
|
||||
`npx supabase db diff -f <migration_name>`
|
||||
|
||||
## Applying migrations
|
||||
|
||||
Reset database in cloud
|
||||
|
||||
> Prerequisite login using `npx supabase login`
|
||||
|
||||
`npx supabase db reset --linked`
|
||||
@@ -0,0 +1,35 @@
|
||||
# Stripe webhook event listening
|
||||
|
||||
> First ensure you've installed the [Stripe CLI](https://docs.stripe.com/stripe-cli)
|
||||
|
||||
Log in using the stripe CLI:
|
||||
|
||||
```bash
|
||||
stripe login
|
||||
```
|
||||
|
||||
Obtain your Webhook signing secret:
|
||||
|
||||
```bash
|
||||
stripe listen
|
||||
```
|
||||
|
||||
Add the secret key to to your .env(.local):
|
||||
|
||||
`PUBLIC_STRIPE_ENDPOINT_SECRET`
|
||||
|
||||
### Testing the webhook locally
|
||||
|
||||
You can use the [Stripe CLI to forward events to your local server](https://docs.stripe.com/webhooks):
|
||||
|
||||
```bash
|
||||
stripe listen --forward-to localhost:5173/api/webhook/stripe
|
||||
```
|
||||
|
||||
To disable HTTPS certificate verification, use the --skip-verify optional flag.
|
||||
|
||||
```bash
|
||||
stripe listen --forward-to localhost:5173/api/webhook/stripe --skip-verify
|
||||
```
|
||||
|
||||
TODO - Add instructions for what else to do to get the webhook working locally.
|
||||
Generated
+589
-114
File diff suppressed because it is too large
Load Diff
+10
-7
@@ -3,14 +3,13 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
|
||||
"build": "npx tailwindcss -i ./static/input.css -o ./static/output.css && vite build",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"tailwind": "npx tailwindcss -i ./static/input.css -o ./static/output.css"
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
@@ -19,6 +18,8 @@
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.6",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
@@ -30,9 +31,11 @@
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss-load-config": "^5.1.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"supabase": "^1.178.2",
|
||||
"svelte": "^4.2.8",
|
||||
"svelte-check": "^3.6.2",
|
||||
"tailwindcss": "^3.4.3",
|
||||
@@ -41,8 +44,8 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@supabase/supabase-js": "^2.42.0",
|
||||
"@supabase/ssr": "^0.4.0",
|
||||
"@supabase/supabase-js": "^2.44.2",
|
||||
"stripe": "^15.4.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
plugins: [require('tailwindcss'), require('autoprefixer')],
|
||||
};
|
||||
Vendored
+6
-5
@@ -1,22 +1,23 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
import { SupabaseClient, Session, User } from '@supabase/supabase-js'
|
||||
import { SupabaseClient, Session, User } from '@supabase/supabase-js';
|
||||
import type { Database } from '$types/supabase';
|
||||
declare global {
|
||||
const __DATE__: string;
|
||||
const __RELOAD_SW__: boolean;
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
supabase: SupabaseClient
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
|
||||
supabase: SupabaseClient<Database>;
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>;
|
||||
userid: string;
|
||||
buildDate: string;
|
||||
periodicUpdates: boolean;
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
}
|
||||
session: Session | null;
|
||||
}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
<link rel="stylesheet" href="%sveltekit.assets%/output.css" />
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/* Write your global styles here, in PostCSS syntax */
|
||||
@@ -1,3 +1,4 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
+20
-17
@@ -5,45 +5,48 @@ import type { Handle } from '@sveltejs/kit';
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
cookies: {
|
||||
get: (key) => event.cookies.get(key),
|
||||
getAll: () => event.cookies.getAll(),
|
||||
/**
|
||||
* Note: You have to add the `path` variable to the
|
||||
* set and remove method due to sveltekit's cookie API
|
||||
* requiring this to be set, setting the path to an empty string
|
||||
* will replicate previous/standard behaviour (https://kit.svelte.dev/docs/types#public-types-cookies)
|
||||
* SvelteKit's cookies API requires `path` to be explicitly set in
|
||||
* the cookie options. Setting `path` to `/` replicates previous/
|
||||
* standard behavior.
|
||||
*/
|
||||
set: (key, value, options) => {
|
||||
event.cookies.set(key, value, { ...options, path: '/' });
|
||||
},
|
||||
remove: (key, options) => {
|
||||
event.cookies.delete(key, { ...options, path: '/' });
|
||||
setAll: (cookiesToSet) => {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
event.cookies.set(name, value, { ...options, path: '/' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
* Unlike `supabase.auth.getSession()`, which returns the session _without_
|
||||
* validating the JWT, this function also calls `getUser()` to validate the
|
||||
* JWT before returning the session.
|
||||
*/
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { session }
|
||||
} = await event.locals.supabase.auth.getSession();
|
||||
if (!session) {
|
||||
return { session: null, user: null };
|
||||
}
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error
|
||||
} = await event.locals.supabase.auth.getUser();
|
||||
if (error) {
|
||||
// JWT validation has failed
|
||||
return { session: null, user: null };
|
||||
}
|
||||
|
||||
const {
|
||||
data: { session }
|
||||
} = await event.locals.supabase.auth.getSession();
|
||||
return { session, user };
|
||||
};
|
||||
|
||||
return resolve(event, {
|
||||
filterSerializedResponseHeaders(name) {
|
||||
return name === 'content-range';
|
||||
return name === 'content-range' || name === 'x-supabase-api-version';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<script lang="ts">
|
||||
import stripe from 'stripe';
|
||||
import { PUBLIC_STRIPE_SECRET_KEY } from '$env/static/public';
|
||||
|
||||
let email = '';
|
||||
let password = '';
|
||||
|
||||
// Access the supabase client from the layout data
|
||||
export let supabase: any;
|
||||
|
||||
const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY);
|
||||
|
||||
async function signUpNewUser() {
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
@@ -12,7 +17,7 @@
|
||||
password: password,
|
||||
options: {
|
||||
// Redirect URL after successful sign-up
|
||||
redirectTo: '/welcome'
|
||||
redirectTo: '/account'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,6 +25,21 @@
|
||||
throw error;
|
||||
}
|
||||
|
||||
const customer = await stripeClient.customers.create({
|
||||
email
|
||||
});
|
||||
|
||||
const response = await supabase
|
||||
.from('user')
|
||||
.update({
|
||||
customer_id: customer.id
|
||||
})
|
||||
.match({
|
||||
id: data.user.id
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
|
||||
// Handle success (optional)
|
||||
} catch (error: any) {
|
||||
console.error('Sign up error:', error.message);
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import stripe from 'stripe';
|
||||
import type { Database, Tables, TablesInsert } from '$types/supabase';
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_STRIPE_SECRET_KEY } from '$env/static/public';
|
||||
import { SUPABASE_SERVICE_ROLE_KEY } from '$env/static/private';
|
||||
|
||||
const toDateTime = (secs: number) => {
|
||||
const t = new Date(+0); // Unix epoch start.
|
||||
t.setSeconds(secs);
|
||||
return t;
|
||||
};
|
||||
|
||||
const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY);
|
||||
|
||||
type Product = Tables<'products'>;
|
||||
type Price = Tables<'prices'>;
|
||||
|
||||
// Change to control trial period length
|
||||
const TRIAL_PERIOD_DAYS = 0;
|
||||
|
||||
// Note: supabaseAdmin uses the SERVICE_ROLE_KEY which you must only use in a secure server-side context
|
||||
// as it has admin privileges and overwrites RLS policies!
|
||||
const supabaseAdmin = createClient<Database>(
|
||||
PUBLIC_SUPABASE_URL || '',
|
||||
SUPABASE_SERVICE_ROLE_KEY || ''
|
||||
);
|
||||
|
||||
const upsertProductRecord = async (product: stripe.Product) => {
|
||||
const productData: Product = {
|
||||
id: product.id,
|
||||
active: product.active,
|
||||
name: product.name,
|
||||
description: product.description ?? null,
|
||||
image: product.images?.[0] ?? null,
|
||||
metadata: product.metadata
|
||||
};
|
||||
|
||||
const { error: upsertError } = await supabaseAdmin.from('products').upsert([productData]);
|
||||
if (upsertError) throw new Error(`Product insert/update failed: ${upsertError.message}`);
|
||||
console.log(`Product inserted/updated: ${product.id}`);
|
||||
};
|
||||
|
||||
const upsertPriceRecord = async (price: stripe.Price, retryCount = 0, maxRetries = 3) => {
|
||||
const priceData: Price = {
|
||||
id: price.id,
|
||||
product_id: typeof price.product === 'string' ? price.product : '',
|
||||
active: price.active,
|
||||
currency: price.currency,
|
||||
type: price.type,
|
||||
unit_amount: price.unit_amount ?? null,
|
||||
interval: price.recurring?.interval ?? null,
|
||||
interval_count: price.recurring?.interval_count ?? null,
|
||||
trial_period_days: price.recurring?.trial_period_days ?? TRIAL_PERIOD_DAYS
|
||||
};
|
||||
|
||||
const { error: upsertError } = await supabaseAdmin.from('prices').upsert([priceData]);
|
||||
|
||||
if (upsertError?.message.includes('foreign key constraint')) {
|
||||
if (retryCount < maxRetries) {
|
||||
console.log(`Retry attempt ${retryCount + 1} for price ID: ${price.id}`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await upsertPriceRecord(price, retryCount + 1, maxRetries);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Price insert/update failed after ${maxRetries} retries: ${upsertError.message}`
|
||||
);
|
||||
}
|
||||
} else if (upsertError) {
|
||||
throw new Error(`Price insert/update failed: ${upsertError.message}`);
|
||||
} else {
|
||||
console.log(`Price inserted/updated: ${price.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteProductRecord = async (product: stripe.Product) => {
|
||||
const { error: deletionError } = await supabaseAdmin
|
||||
.from('products')
|
||||
.delete()
|
||||
.eq('id', product.id);
|
||||
if (deletionError) throw new Error(`Product deletion failed: ${deletionError.message}`);
|
||||
console.log(`Product deleted: ${product.id}`);
|
||||
};
|
||||
|
||||
const deletePriceRecord = async (price: stripe.Price) => {
|
||||
const { error: deletionError } = await supabaseAdmin.from('prices').delete().eq('id', price.id);
|
||||
if (deletionError) throw new Error(`Price deletion failed: ${deletionError.message}`);
|
||||
console.log(`Price deleted: ${price.id}`);
|
||||
};
|
||||
|
||||
const upsertCustomerToSupabase = async (uuid: string, customerId: string) => {
|
||||
const { error: upsertError } = await supabaseAdmin
|
||||
.from('customers')
|
||||
.upsert([{ id: uuid, stripe_customer_id: customerId }]);
|
||||
|
||||
if (upsertError)
|
||||
throw new Error(`Supabase customer record creation failed: ${upsertError.message}`);
|
||||
|
||||
return customerId;
|
||||
};
|
||||
|
||||
const createCustomerInStripe = async (uuid: string, email: string) => {
|
||||
const customerData = { metadata: { supabaseUUID: uuid }, email: email };
|
||||
const newCustomer = await stripeClient.customers.create(customerData);
|
||||
if (!newCustomer) throw new Error('Stripe customer creation failed.');
|
||||
|
||||
return newCustomer.id;
|
||||
};
|
||||
|
||||
const createOrRetrieveCustomer = async ({ email, uuid }: { email: string; uuid: string }) => {
|
||||
// Check if the customer already exists in Supabase
|
||||
const { data: existingSupabaseCustomer, error: queryError } = await supabaseAdmin
|
||||
.from('customers')
|
||||
.select('*')
|
||||
.eq('id', uuid)
|
||||
.maybeSingle();
|
||||
|
||||
if (queryError) {
|
||||
throw new Error(`Supabase customer lookup failed: ${queryError.message}`);
|
||||
}
|
||||
|
||||
// Retrieve the Stripe customer ID using the Supabase customer ID, with email fallback
|
||||
let stripeCustomerId: string | undefined;
|
||||
if (existingSupabaseCustomer?.stripe_customer_id) {
|
||||
const existingStripeCustomer = await stripeClient.customers.retrieve(
|
||||
existingSupabaseCustomer.stripe_customer_id
|
||||
);
|
||||
stripeCustomerId = existingStripeCustomer.id;
|
||||
} else {
|
||||
// If Stripe ID is missing from Supabase, try to retrieve Stripe customer ID by email
|
||||
const stripeCustomers = await stripeClient.customers.list({ email: email });
|
||||
stripeCustomerId = stripeCustomers.data.length > 0 ? stripeCustomers.data[0].id : undefined;
|
||||
}
|
||||
|
||||
// If still no stripeCustomerId, create a new customer in Stripe
|
||||
const stripeIdToInsert = stripeCustomerId
|
||||
? stripeCustomerId
|
||||
: await createCustomerInStripe(uuid, email);
|
||||
if (!stripeIdToInsert) throw new Error('Stripe customer creation failed.');
|
||||
|
||||
if (existingSupabaseCustomer && stripeCustomerId) {
|
||||
// If Supabase has a record but doesn't match Stripe, update Supabase record
|
||||
if (existingSupabaseCustomer.stripe_customer_id !== stripeCustomerId) {
|
||||
const { error: updateError } = await supabaseAdmin
|
||||
.from('customers')
|
||||
.update({ stripe_customer_id: stripeCustomerId })
|
||||
.eq('id', uuid);
|
||||
|
||||
if (updateError)
|
||||
throw new Error(`Supabase customer record update failed: ${updateError.message}`);
|
||||
console.warn(`Supabase customer record mismatched Stripe ID. Supabase record updated.`);
|
||||
}
|
||||
// If Supabase has a record and matches Stripe, return Stripe customer ID
|
||||
return stripeCustomerId;
|
||||
} else {
|
||||
console.warn(`Supabase customer record was missing. A new record was created.`);
|
||||
|
||||
// If Supabase has no record, create a new record and return Stripe customer ID
|
||||
const upsertedStripeCustomer = await upsertCustomerToSupabase(uuid, stripeIdToInsert);
|
||||
if (!upsertedStripeCustomer) throw new Error('Supabase customer record creation failed.');
|
||||
|
||||
return upsertedStripeCustomer;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copies the billing details from the payment method to the customer object.
|
||||
*/
|
||||
const copyBillingDetailsToCustomer = async (uuid: string, payment_method: stripe.PaymentMethod) => {
|
||||
//Todo: check this assertion
|
||||
const customer = payment_method.customer as string;
|
||||
const { name, phone, address } = payment_method.billing_details;
|
||||
if (!name || !phone || !address) return;
|
||||
|
||||
await stripeClient.customers.update(customer, { name, phone, address });
|
||||
const { error: updateError } = await supabaseAdmin
|
||||
.from('users')
|
||||
.update({
|
||||
billing_address: { ...address },
|
||||
payment_method: { ...payment_method[payment_method.type] }
|
||||
})
|
||||
.eq('id', uuid);
|
||||
if (updateError) throw new Error(`Customer update failed: ${updateError.message}`);
|
||||
};
|
||||
|
||||
const manageSubscriptionStatusChange = async (
|
||||
subscriptionId: string,
|
||||
customerId: string,
|
||||
createAction = false
|
||||
) => {
|
||||
// Get customer's UUID from mapping table.
|
||||
const { data: customerData, error: noCustomerError } = await supabaseAdmin
|
||||
.from('customers')
|
||||
.select('id')
|
||||
.eq('stripe_customer_id', customerId)
|
||||
.single();
|
||||
|
||||
if (noCustomerError) throw new Error(`Customer lookup failed: ${noCustomerError.message}`);
|
||||
|
||||
const { id: uuid } = customerData!;
|
||||
|
||||
const subscription = await stripeClient.subscriptions.retrieve(subscriptionId, {
|
||||
expand: ['default_payment_method']
|
||||
});
|
||||
// Upsert the latest status of the subscription object.
|
||||
const subscriptionData: TablesInsert<'subscriptions'> = {
|
||||
id: subscription.id,
|
||||
user_id: uuid,
|
||||
metadata: subscription.metadata,
|
||||
status: subscription.status,
|
||||
price_id: subscription.items.data[0].price.id,
|
||||
//TODO check quantity on subscription
|
||||
|
||||
quantity: subscription.quantity,
|
||||
cancel_at_period_end: subscription.cancel_at_period_end,
|
||||
cancel_at: subscription.cancel_at ? toDateTime(subscription.cancel_at).toISOString() : null,
|
||||
canceled_at: subscription.canceled_at
|
||||
? toDateTime(subscription.canceled_at).toISOString()
|
||||
: null,
|
||||
current_period_start: toDateTime(subscription.current_period_start).toISOString(),
|
||||
current_period_end: toDateTime(subscription.current_period_end).toISOString(),
|
||||
created: toDateTime(subscription.created).toISOString(),
|
||||
ended_at: subscription.ended_at ? toDateTime(subscription.ended_at).toISOString() : null,
|
||||
trial_start: subscription.trial_start
|
||||
? toDateTime(subscription.trial_start).toISOString()
|
||||
: null,
|
||||
trial_end: subscription.trial_end ? toDateTime(subscription.trial_end).toISOString() : null
|
||||
};
|
||||
|
||||
const { error: upsertError } = await supabaseAdmin
|
||||
.from('subscriptions')
|
||||
.upsert([subscriptionData]);
|
||||
if (upsertError) throw new Error(`Subscription insert/update failed: ${upsertError.message}`);
|
||||
console.log(`Inserted/updated subscription [${subscription.id}] for user [${uuid}]`);
|
||||
|
||||
// For a new subscription copy the billing details to the customer object.
|
||||
// NOTE: This is a costly operation and should happen at the very end.
|
||||
if (createAction && subscription.default_payment_method && uuid)
|
||||
await copyBillingDetailsToCustomer(
|
||||
uuid,
|
||||
subscription.default_payment_method as stripe.PaymentMethod
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
upsertProductRecord,
|
||||
upsertPriceRecord,
|
||||
deleteProductRecord,
|
||||
deletePriceRecord,
|
||||
createOrRetrieveCustomer,
|
||||
manageSubscriptionStatusChange
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
|
||||
const { session, user } = await safeGetSession();
|
||||
|
||||
return {
|
||||
session,
|
||||
user
|
||||
user,
|
||||
cookies: cookies.getAll()
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '../app.postcss';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { isBrowser } from '@supabase/ssr';
|
||||
import { basket, type Basket, type Item } from '$lib/stores/basket.js';
|
||||
@@ -32,8 +32,8 @@
|
||||
let cookiesAccepted = false;
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange((event, _session) => {
|
||||
if (_session?.expires_at !== session?.expires_at) {
|
||||
const { data } = supabase.auth.onAuthStateChange((event, newSession) => {
|
||||
if (newSession?.expires_at !== session?.expires_at) {
|
||||
// tells SvelteKit that the root +layout.ts load function should be executed whenever the session updates to keep the page store in sync.
|
||||
invalidate('supabase:auth');
|
||||
}
|
||||
@@ -138,7 +138,7 @@
|
||||
class="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 text-base-content rounded-box min-w-[13rem] w-auto"
|
||||
>
|
||||
<li>
|
||||
<a href="/profile"> Profile </a>
|
||||
<a href="/account"> Account </a>
|
||||
</li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li><SignOut {supabase} /></li>
|
||||
@@ -265,7 +265,7 @@
|
||||
<span>{localGeneral.toastMessage == '' ? 'Added to basket' : localGeneral.toastMessage}</span>
|
||||
</div>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="postcss">
|
||||
.user-circle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
+16
-15
@@ -1,25 +1,26 @@
|
||||
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr';
|
||||
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { createBrowserClient, isBrowser, parse } from '@supabase/ssr';
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
depends('supabase:auth');
|
||||
|
||||
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch
|
||||
},
|
||||
cookies: {
|
||||
get(key) {
|
||||
if (!isBrowser()) {
|
||||
return JSON.stringify(data.session);
|
||||
const supabase = isBrowser()
|
||||
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch
|
||||
}
|
||||
|
||||
const cookie = parse(document.cookie);
|
||||
return cookie[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch
|
||||
},
|
||||
cookies: {
|
||||
getAll() {
|
||||
return data.cookies;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { type User } from '@supabase/auth-js';
|
||||
import SignUp from '$lib/components/SignUp.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession();
|
||||
|
||||
if (!session) {
|
||||
redirect(303, '/');
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, full_name`)
|
||||
.eq('id', session.user.id)
|
||||
.single();
|
||||
|
||||
return { session, profile };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
update: async ({ request, locals: { supabase, safeGetSession } }) => {
|
||||
const formData = await request.formData();
|
||||
const fullName = formData.get('fullName') as string;
|
||||
|
||||
const { session } = await safeGetSession();
|
||||
|
||||
const { error } = await supabase.from('profiles').upsert({
|
||||
id: session?.user.id,
|
||||
full_name: fullName,
|
||||
username: session?.user.email,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
console.error(error);
|
||||
|
||||
if (error) {
|
||||
return fail(500, {
|
||||
fullName,
|
||||
username: session?.user.email
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
fullName,
|
||||
username: session?.user.email
|
||||
};
|
||||
},
|
||||
signout: async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession();
|
||||
if (session) {
|
||||
await supabase.auth.signOut();
|
||||
redirect(303, '/');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
|
||||
export let data;
|
||||
export let form;
|
||||
|
||||
let { session, supabase, profile } = data;
|
||||
$: ({ session, supabase, profile } = data);
|
||||
|
||||
let profileForm: HTMLFormElement;
|
||||
let loading = false;
|
||||
let fullName: string = profile?.full_name ?? '';
|
||||
|
||||
const handleSubmit: SubmitFunction = () => {
|
||||
loading = true;
|
||||
return async () => {
|
||||
loading = false;
|
||||
};
|
||||
};
|
||||
|
||||
const handleSignOut: SubmitFunction = () => {
|
||||
loading = true;
|
||||
return async ({ update }) => {
|
||||
loading = false;
|
||||
update();
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="form-widget">
|
||||
<form
|
||||
class="form-widget"
|
||||
method="post"
|
||||
action="?/update"
|
||||
use:enhance={handleSubmit}
|
||||
bind:this={profileForm}
|
||||
>
|
||||
<div>
|
||||
<label for="email">Username</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="fullName">Full Name</label>
|
||||
<input id="fullName" name="fullName" type="text" value={form?.fullName ?? fullName} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="button block primary"
|
||||
value={loading ? 'Loading...' : 'Update'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="?/signout" use:enhance={handleSignOut}>
|
||||
<div>
|
||||
<button class="button block" disabled={loading}>Sign Out</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import stripe from 'stripe';
|
||||
import { PUBLIC_STRIPE_SECRET_KEY } from '$env/static/public';
|
||||
import { PUBLIC_STRIPE_ENDPOINT_SECRET } from '$env/static/public';
|
||||
import {
|
||||
deletePriceRecord,
|
||||
deleteProductRecord,
|
||||
manageSubscriptionStatusChange,
|
||||
upsertPriceRecord,
|
||||
upsertProductRecord
|
||||
} from '$lib/utils/supabase/admin';
|
||||
|
||||
const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY);
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const body = await request.text();
|
||||
|
||||
try {
|
||||
let event = JSON.parse(body) as stripe.Event;
|
||||
|
||||
// Get the signature sent by Stripe
|
||||
const signature = request.headers.get('stripe-signature');
|
||||
|
||||
// Only verify the event if you have an endpoint secret defined.
|
||||
// Otherwise use the basic event deserialized with JSON.parse
|
||||
if (PUBLIC_STRIPE_ENDPOINT_SECRET && signature) {
|
||||
try {
|
||||
event = stripeClient.webhooks.constructEvent(
|
||||
body,
|
||||
signature,
|
||||
PUBLIC_STRIPE_ENDPOINT_SECRET
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`⚠️ Webhook signature verification failed.`, err.message);
|
||||
return {
|
||||
status: 400,
|
||||
body: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'product.created':
|
||||
case 'product.updated':
|
||||
await upsertProductRecord(event.data.object as stripe.Product);
|
||||
break;
|
||||
case 'price.created':
|
||||
case 'price.updated':
|
||||
await upsertPriceRecord(event.data.object as stripe.Price);
|
||||
break;
|
||||
case 'price.deleted':
|
||||
await deletePriceRecord(event.data.object as stripe.Price);
|
||||
break;
|
||||
case 'product.deleted':
|
||||
await deleteProductRecord(event.data.object as stripe.Product);
|
||||
break;
|
||||
case 'customer.subscription.created':
|
||||
case 'customer.subscription.updated':
|
||||
case 'customer.subscription.deleted': {
|
||||
const subscription = event.data.object as stripe.Subscription;
|
||||
await manageSubscriptionStatusChange(
|
||||
subscription.id,
|
||||
subscription.customer as string,
|
||||
event.type === 'customer.subscription.created'
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'checkout.session.completed': {
|
||||
const checkoutSession = event.data.object as stripe.Checkout.Session;
|
||||
if (checkoutSession.mode === 'subscription') {
|
||||
const subscriptionId = checkoutSession.subscription;
|
||||
await manageSubscriptionStatusChange(
|
||||
subscriptionId as string,
|
||||
checkoutSession.customer as string,
|
||||
true
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error(`Unhandled event type ${event.type}`);
|
||||
}
|
||||
|
||||
// Return a response to acknowledge receipt of the event
|
||||
return new Response(JSON.stringify({ received: true }), {
|
||||
status: 200
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(`Webhook Error: ${err.message}`);
|
||||
return {
|
||||
status: 400,
|
||||
body: `Webhook Error: ${err.message}`
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<p>Login error</p>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { type User } from '@supabase/auth-js';
|
||||
|
||||
export let data;
|
||||
let { supabase, session } = data;
|
||||
$: ({ supabase, session } = data);
|
||||
</script>
|
||||
|
||||
<div class="h-screen">
|
||||
{#if session?.user}
|
||||
{session?.user?.email}
|
||||
{:else}
|
||||
<p>Please log in to view this page</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
.env
|
||||
@@ -0,0 +1,194 @@
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "SvelteKitSaasBoilerplate"
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||
max_frequency = "1s"
|
||||
|
||||
# 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 = true
|
||||
# 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)"
|
||||
|
||||
# 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
|
||||
|
||||
[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 = false
|
||||
port = 54327
|
||||
vector_port = 54328
|
||||
# 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)"
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* USERS
|
||||
* Note: This table contains user data. Users should only be able to view and update their own data.
|
||||
*/
|
||||
create table users (
|
||||
-- UUID from auth.users
|
||||
id uuid references auth.users not null primary key,
|
||||
full_name text,
|
||||
avatar_url text,
|
||||
-- The customer's billing address, stored in JSON format.
|
||||
billing_address jsonb,
|
||||
-- Stores your customer's payment instruments.
|
||||
payment_method jsonb
|
||||
);
|
||||
alter table users
|
||||
enable row level security;
|
||||
create policy "Can view own user data." on users
|
||||
for select using ((select auth.uid()) = id);
|
||||
create policy "Can update own user data." on users
|
||||
for update using ((select auth.uid()) = id);
|
||||
|
||||
/**
|
||||
* This trigger automatically creates a user entry when a new user signs up via Supabase Auth.
|
||||
*/
|
||||
create function public.handle_new_user()
|
||||
returns trigger as
|
||||
$$
|
||||
begin
|
||||
insert into public.users (id, full_name, avatar_url)
|
||||
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
|
||||
return new;
|
||||
end;
|
||||
$$
|
||||
language plpgsql security definer;
|
||||
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row
|
||||
execute procedure public.handle_new_user();
|
||||
|
||||
/**
|
||||
* CUSTOMERS
|
||||
* Note: this is a private table that contains a mapping of user IDs to Stripe customer IDs.
|
||||
*/
|
||||
create table customers (
|
||||
-- UUID from auth.users
|
||||
id uuid references auth.users not null primary key,
|
||||
-- The user's customer ID in Stripe. User must not be able to update this.
|
||||
stripe_customer_id text
|
||||
);
|
||||
alter table customers enable row level security;
|
||||
-- No policies as this is a private table that the user must not have access to.
|
||||
|
||||
/**
|
||||
* PRODUCTS
|
||||
* Note: products are created and managed in Stripe and synced to our DB via Stripe webhooks.
|
||||
*/
|
||||
create table products (
|
||||
-- Product ID from Stripe, e.g. prod_1234.
|
||||
id text primary key,
|
||||
-- Whether the product is currently available for purchase.
|
||||
active boolean,
|
||||
-- The product's name, meant to be displayable to the customer. Whenever this product is sold via a subscription, name will show up on associated invoice line item descriptions.
|
||||
name text,
|
||||
-- The product's description, meant to be displayable to the customer. Use this field to optionally store a long form explanation of the product being sold for your own rendering purposes.
|
||||
description text,
|
||||
-- A URL of the product image in Stripe, meant to be displayable to the customer.
|
||||
image text,
|
||||
-- Set of key-value pairs, used to store additional information about the object in a structured format.
|
||||
metadata jsonb
|
||||
);
|
||||
alter table products
|
||||
enable row level security;
|
||||
create policy "Allow public read-only access." on products
|
||||
for select using (true);
|
||||
|
||||
/**
|
||||
* PRICES
|
||||
* Note: prices are created and managed in Stripe and synced to our DB via Stripe webhooks.
|
||||
*/
|
||||
create type pricing_type as enum ('one_time', 'recurring');
|
||||
create type pricing_plan_interval as enum ('day', 'week', 'month', 'year');
|
||||
create table prices (
|
||||
-- Price ID from Stripe, e.g. price_1234.
|
||||
id text primary key,
|
||||
-- The ID of the prduct that this price belongs to.
|
||||
product_id text references products,
|
||||
-- Whether the price can be used for new purchases.
|
||||
active boolean,
|
||||
-- A brief description of the price.
|
||||
description text,
|
||||
-- The unit amount as a positive integer in the smallest currency unit (e.g., 100 cents for US$1.00 or 100 for ¥100, a zero-decimal currency).
|
||||
unit_amount bigint,
|
||||
-- Three-letter ISO currency code, in lowercase.
|
||||
currency text check (char_length(currency) = 3),
|
||||
-- One of `one_time` or `recurring` depending on whether the price is for a one-time purchase or a recurring (subscription) purchase.
|
||||
type pricing_type,
|
||||
-- The frequency at which a subscription is billed. One of `day`, `week`, `month` or `year`.
|
||||
interval pricing_plan_interval,
|
||||
-- The number of intervals (specified in the `interval` attribute) between subscription billings. For example, `interval=month` and `interval_count=3` bills every 3 months.
|
||||
interval_count integer,
|
||||
-- Default number of trial days when subscribing a customer to this price using [`trial_from_plan=true`](https://stripe.com/docs/api#create_subscription-trial_from_plan).
|
||||
trial_period_days integer,
|
||||
-- Set of key-value pairs, used to store additional information about the object in a structured format.
|
||||
metadata jsonb
|
||||
);
|
||||
alter table prices
|
||||
enable row level security;
|
||||
create policy "Allow public read-only access." on prices
|
||||
for select using (true);
|
||||
|
||||
/**
|
||||
* SUBSCRIPTIONS
|
||||
* Note: subscriptions are created and managed in Stripe and synced to our DB via Stripe webhooks.
|
||||
*/
|
||||
create type subscription_status as enum ('trialing', 'active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid');
|
||||
create table subscriptions (
|
||||
-- Subscription ID from Stripe, e.g. sub_1234.
|
||||
id text primary key,
|
||||
user_id uuid references auth.users not null,
|
||||
-- The status of the subscription object, one of subscription_status type above.
|
||||
status subscription_status,
|
||||
-- Set of key-value pairs, used to store additional information about the object in a structured format.
|
||||
metadata jsonb,
|
||||
-- ID of the price that created this subscription.
|
||||
price_id text references prices,
|
||||
-- Quantity multiplied by the unit amount of the price creates the amount of the subscription. Can be used to charge multiple seats.
|
||||
quantity integer,
|
||||
-- If true the subscription has been canceled by the user and will be deleted at the end of the billing period.
|
||||
cancel_at_period_end boolean,
|
||||
-- Time at which the subscription was created.
|
||||
created timestamp with time zone default timezone('utc'::text, now()) not null,
|
||||
-- Start of the current period that the subscription has been invoiced for.
|
||||
current_period_start timestamp with time zone default timezone('utc'::text, now()) not null,
|
||||
-- End of the current period that the subscription has been invoiced for. At the end of this period, a new invoice will be created.
|
||||
current_period_end timestamp with time zone default timezone('utc'::text, now()) not null,
|
||||
-- If the subscription has ended, the timestamp of the date the subscription ended.
|
||||
ended_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
-- A date in the future at which the subscription will automatically get canceled.
|
||||
cancel_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
-- If the subscription has been canceled, the date of that cancellation. If the subscription was canceled with `cancel_at_period_end`, `canceled_at` will still reflect the date of the initial cancellation request, not the end of the subscription period when the subscription is automatically moved to a canceled state.
|
||||
canceled_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
-- If the subscription has a trial, the beginning of that trial.
|
||||
trial_start timestamp with time zone default timezone('utc'::text, now()),
|
||||
-- If the subscription has a trial, the end of that trial.
|
||||
trial_end timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
alter table subscriptions
|
||||
enable row level security;
|
||||
create policy "Can only view own subs data." on subscriptions
|
||||
for select using ((select auth.uid()) = user_id);
|
||||
|
||||
/**
|
||||
* REALTIME SUBSCRIPTIONS
|
||||
* Only allow realtime listening on public tables.
|
||||
*/
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime
|
||||
for table products, prices;
|
||||
Vendored
+653
@@ -0,0 +1,653 @@
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
graphql_public: {
|
||||
Tables: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
graphql: {
|
||||
Args: {
|
||||
operationName?: string
|
||||
query?: string
|
||||
variables?: Json
|
||||
extensions?: Json
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
customers: {
|
||||
Row: {
|
||||
id: string
|
||||
stripe_customer_id: string | null
|
||||
}
|
||||
Insert: {
|
||||
id: string
|
||||
stripe_customer_id?: string | null
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
stripe_customer_id?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "customers_id_fkey"
|
||||
columns: ["id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
prices: {
|
||||
Row: {
|
||||
active: boolean | null
|
||||
currency: string | null
|
||||
description: string | null
|
||||
id: string
|
||||
interval: Database["public"]["Enums"]["pricing_plan_interval"] | null
|
||||
interval_count: number | null
|
||||
metadata: Json | null
|
||||
product_id: string | null
|
||||
trial_period_days: number | null
|
||||
type: Database["public"]["Enums"]["pricing_type"] | null
|
||||
unit_amount: number | null
|
||||
}
|
||||
Insert: {
|
||||
active?: boolean | null
|
||||
currency?: string | null
|
||||
description?: string | null
|
||||
id: string
|
||||
interval?: Database["public"]["Enums"]["pricing_plan_interval"] | null
|
||||
interval_count?: number | null
|
||||
metadata?: Json | null
|
||||
product_id?: string | null
|
||||
trial_period_days?: number | null
|
||||
type?: Database["public"]["Enums"]["pricing_type"] | null
|
||||
unit_amount?: number | null
|
||||
}
|
||||
Update: {
|
||||
active?: boolean | null
|
||||
currency?: string | null
|
||||
description?: string | null
|
||||
id?: string
|
||||
interval?: Database["public"]["Enums"]["pricing_plan_interval"] | null
|
||||
interval_count?: number | null
|
||||
metadata?: Json | null
|
||||
product_id?: string | null
|
||||
trial_period_days?: number | null
|
||||
type?: Database["public"]["Enums"]["pricing_type"] | null
|
||||
unit_amount?: number | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "prices_product_id_fkey"
|
||||
columns: ["product_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "products"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
products: {
|
||||
Row: {
|
||||
active: boolean | null
|
||||
description: string | null
|
||||
id: string
|
||||
image: string | null
|
||||
metadata: Json | null
|
||||
name: string | null
|
||||
}
|
||||
Insert: {
|
||||
active?: boolean | null
|
||||
description?: string | null
|
||||
id: string
|
||||
image?: string | null
|
||||
metadata?: Json | null
|
||||
name?: string | null
|
||||
}
|
||||
Update: {
|
||||
active?: boolean | null
|
||||
description?: string | null
|
||||
id?: string
|
||||
image?: string | null
|
||||
metadata?: Json | null
|
||||
name?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
subscriptions: {
|
||||
Row: {
|
||||
cancel_at: string | null
|
||||
cancel_at_period_end: boolean | null
|
||||
canceled_at: string | null
|
||||
created: string
|
||||
current_period_end: string
|
||||
current_period_start: string
|
||||
ended_at: string | null
|
||||
id: string
|
||||
metadata: Json | null
|
||||
price_id: string | null
|
||||
quantity: number | null
|
||||
status: Database["public"]["Enums"]["subscription_status"] | null
|
||||
trial_end: string | null
|
||||
trial_start: string | null
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
cancel_at?: string | null
|
||||
cancel_at_period_end?: boolean | null
|
||||
canceled_at?: string | null
|
||||
created?: string
|
||||
current_period_end?: string
|
||||
current_period_start?: string
|
||||
ended_at?: string | null
|
||||
id: string
|
||||
metadata?: Json | null
|
||||
price_id?: string | null
|
||||
quantity?: number | null
|
||||
status?: Database["public"]["Enums"]["subscription_status"] | null
|
||||
trial_end?: string | null
|
||||
trial_start?: string | null
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
cancel_at?: string | null
|
||||
cancel_at_period_end?: boolean | null
|
||||
canceled_at?: string | null
|
||||
created?: string
|
||||
current_period_end?: string
|
||||
current_period_start?: string
|
||||
ended_at?: string | null
|
||||
id?: string
|
||||
metadata?: Json | null
|
||||
price_id?: string | null
|
||||
quantity?: number | null
|
||||
status?: Database["public"]["Enums"]["subscription_status"] | null
|
||||
trial_end?: string | null
|
||||
trial_start?: string | null
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "subscriptions_price_id_fkey"
|
||||
columns: ["price_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "prices"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "subscriptions_user_id_fkey"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
users: {
|
||||
Row: {
|
||||
avatar_url: string | null
|
||||
billing_address: Json | null
|
||||
full_name: string | null
|
||||
id: string
|
||||
payment_method: Json | null
|
||||
}
|
||||
Insert: {
|
||||
avatar_url?: string | null
|
||||
billing_address?: Json | null
|
||||
full_name?: string | null
|
||||
id: string
|
||||
payment_method?: Json | null
|
||||
}
|
||||
Update: {
|
||||
avatar_url?: string | null
|
||||
billing_address?: Json | null
|
||||
full_name?: string | null
|
||||
id?: string
|
||||
payment_method?: Json | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "users_id_fkey"
|
||||
columns: ["id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Enums: {
|
||||
pricing_plan_interval: "day" | "week" | "month" | "year"
|
||||
pricing_type: "one_time" | "recurring"
|
||||
subscription_status:
|
||||
| "trialing"
|
||||
| "active"
|
||||
| "canceled"
|
||||
| "incomplete"
|
||||
| "incomplete_expired"
|
||||
| "past_due"
|
||||
| "unpaid"
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
storage: {
|
||||
Tables: {
|
||||
buckets: {
|
||||
Row: {
|
||||
allowed_mime_types: string[] | null
|
||||
avif_autodetection: boolean | null
|
||||
created_at: string | null
|
||||
file_size_limit: number | null
|
||||
id: string
|
||||
name: string
|
||||
owner: string | null
|
||||
owner_id: string | null
|
||||
public: boolean | null
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
allowed_mime_types?: string[] | null
|
||||
avif_autodetection?: boolean | null
|
||||
created_at?: string | null
|
||||
file_size_limit?: number | null
|
||||
id: string
|
||||
name: string
|
||||
owner?: string | null
|
||||
owner_id?: string | null
|
||||
public?: boolean | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
allowed_mime_types?: string[] | null
|
||||
avif_autodetection?: boolean | null
|
||||
created_at?: string | null
|
||||
file_size_limit?: number | null
|
||||
id?: string
|
||||
name?: string
|
||||
owner?: string | null
|
||||
owner_id?: string | null
|
||||
public?: boolean | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
migrations: {
|
||||
Row: {
|
||||
executed_at: string | null
|
||||
hash: string
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
Insert: {
|
||||
executed_at?: string | null
|
||||
hash: string
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
Update: {
|
||||
executed_at?: string | null
|
||||
hash?: string
|
||||
id?: number
|
||||
name?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
objects: {
|
||||
Row: {
|
||||
bucket_id: string | null
|
||||
created_at: string | null
|
||||
id: string
|
||||
last_accessed_at: string | null
|
||||
metadata: Json | null
|
||||
name: string | null
|
||||
owner: string | null
|
||||
owner_id: string | null
|
||||
path_tokens: string[] | null
|
||||
updated_at: string | null
|
||||
version: string | null
|
||||
}
|
||||
Insert: {
|
||||
bucket_id?: string | null
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
last_accessed_at?: string | null
|
||||
metadata?: Json | null
|
||||
name?: string | null
|
||||
owner?: string | null
|
||||
owner_id?: string | null
|
||||
path_tokens?: string[] | null
|
||||
updated_at?: string | null
|
||||
version?: string | null
|
||||
}
|
||||
Update: {
|
||||
bucket_id?: string | null
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
last_accessed_at?: string | null
|
||||
metadata?: Json | null
|
||||
name?: string | null
|
||||
owner?: string | null
|
||||
owner_id?: string | null
|
||||
path_tokens?: string[] | null
|
||||
updated_at?: string | null
|
||||
version?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "objects_bucketId_fkey"
|
||||
columns: ["bucket_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "buckets"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
s3_multipart_uploads: {
|
||||
Row: {
|
||||
bucket_id: string
|
||||
created_at: string
|
||||
id: string
|
||||
in_progress_size: number
|
||||
key: string
|
||||
owner_id: string | null
|
||||
upload_signature: string
|
||||
version: string
|
||||
}
|
||||
Insert: {
|
||||
bucket_id: string
|
||||
created_at?: string
|
||||
id: string
|
||||
in_progress_size?: number
|
||||
key: string
|
||||
owner_id?: string | null
|
||||
upload_signature: string
|
||||
version: string
|
||||
}
|
||||
Update: {
|
||||
bucket_id?: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
in_progress_size?: number
|
||||
key?: string
|
||||
owner_id?: string | null
|
||||
upload_signature?: string
|
||||
version?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "s3_multipart_uploads_bucket_id_fkey"
|
||||
columns: ["bucket_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "buckets"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
s3_multipart_uploads_parts: {
|
||||
Row: {
|
||||
bucket_id: string
|
||||
created_at: string
|
||||
etag: string
|
||||
id: string
|
||||
key: string
|
||||
owner_id: string | null
|
||||
part_number: number
|
||||
size: number
|
||||
upload_id: string
|
||||
version: string
|
||||
}
|
||||
Insert: {
|
||||
bucket_id: string
|
||||
created_at?: string
|
||||
etag: string
|
||||
id?: string
|
||||
key: string
|
||||
owner_id?: string | null
|
||||
part_number: number
|
||||
size?: number
|
||||
upload_id: string
|
||||
version: string
|
||||
}
|
||||
Update: {
|
||||
bucket_id?: string
|
||||
created_at?: string
|
||||
etag?: string
|
||||
id?: string
|
||||
key?: string
|
||||
owner_id?: string | null
|
||||
part_number?: number
|
||||
size?: number
|
||||
upload_id?: string
|
||||
version?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "s3_multipart_uploads_parts_bucket_id_fkey"
|
||||
columns: ["bucket_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "buckets"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "s3_multipart_uploads_parts_upload_id_fkey"
|
||||
columns: ["upload_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "s3_multipart_uploads"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
can_insert_object: {
|
||||
Args: {
|
||||
bucketid: string
|
||||
name: string
|
||||
owner: string
|
||||
metadata: Json
|
||||
}
|
||||
Returns: undefined
|
||||
}
|
||||
extension: {
|
||||
Args: {
|
||||
name: string
|
||||
}
|
||||
Returns: string
|
||||
}
|
||||
filename: {
|
||||
Args: {
|
||||
name: string
|
||||
}
|
||||
Returns: string
|
||||
}
|
||||
foldername: {
|
||||
Args: {
|
||||
name: string
|
||||
}
|
||||
Returns: string[]
|
||||
}
|
||||
get_size_by_bucket: {
|
||||
Args: Record<PropertyKey, never>
|
||||
Returns: {
|
||||
size: number
|
||||
bucket_id: string
|
||||
}[]
|
||||
}
|
||||
list_multipart_uploads_with_delimiter: {
|
||||
Args: {
|
||||
bucket_id: string
|
||||
prefix_param: string
|
||||
delimiter_param: string
|
||||
max_keys?: number
|
||||
next_key_token?: string
|
||||
next_upload_token?: string
|
||||
}
|
||||
Returns: {
|
||||
key: string
|
||||
id: string
|
||||
created_at: string
|
||||
}[]
|
||||
}
|
||||
list_objects_with_delimiter: {
|
||||
Args: {
|
||||
bucket_id: string
|
||||
prefix_param: string
|
||||
delimiter_param: string
|
||||
max_keys?: number
|
||||
start_after?: string
|
||||
next_token?: string
|
||||
}
|
||||
Returns: {
|
||||
name: string
|
||||
id: string
|
||||
metadata: Json
|
||||
updated_at: string
|
||||
}[]
|
||||
}
|
||||
operation: {
|
||||
Args: Record<PropertyKey, never>
|
||||
Returns: string
|
||||
}
|
||||
search: {
|
||||
Args: {
|
||||
prefix: string
|
||||
bucketname: string
|
||||
limits?: number
|
||||
levels?: number
|
||||
offsets?: number
|
||||
search?: string
|
||||
sortcolumn?: string
|
||||
sortorder?: string
|
||||
}
|
||||
Returns: {
|
||||
name: string
|
||||
id: string
|
||||
updated_at: string
|
||||
created_at: string
|
||||
last_accessed_at: string
|
||||
metadata: Json
|
||||
}[]
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PublicSchema = Database[Extract<keyof Database, "public">]
|
||||
|
||||
export type Tables<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
|
||||
PublicSchema["Views"])
|
||||
? (PublicSchema["Tables"] &
|
||||
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesInsert<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof PublicSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesUpdate<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof PublicSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never
|
||||
|
||||
export type Enums<
|
||||
PublicEnumNameOrOptions extends
|
||||
| keyof PublicSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
> = PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
|
||||
? PublicSchema["Enums"][PublicEnumNameOrOptions]
|
||||
: never
|
||||
|
||||
Reference in New Issue
Block a user