From 822b8c723524ca691fcf0f392ec6ad652c3d69e3 Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 19:18:26 +0100
Subject: [PATCH 01/22] docs(#14): Add db setup instructions
---
docs/database-setup.md | 165 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 165 insertions(+)
create mode 100644 docs/database-setup.md
diff --git a/docs/database-setup.md b/docs/database-setup.md
new file mode 100644
index 0000000..c874754
--- /dev/null
+++ b/docs/database-setup.md
@@ -0,0 +1,165 @@
+# 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. 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;
+```
From 1303c97f728b901112eca2f6e3afce5a0d5d606b Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 19:22:03 +0100
Subject: [PATCH 02/22] docs(#14): Add type generation docs
---
docs/database-setup.md | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/docs/database-setup.md b/docs/database-setup.md
index c874754..7893d1b 100644
--- a/docs/database-setup.md
+++ b/docs/database-setup.md
@@ -163,3 +163,17 @@ drop publication if exists supabase_realtime;
create publication supabase_realtime
for table products, prices;
```
+
+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`
From 66698445f2d44dcdaba6f68116020a2826bd6973 Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 19:22:39 +0100
Subject: [PATCH 03/22] feat(#14): Update signup component to create a stripe
customer on signup
---
src/lib/components/SignUp.svelte | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/src/lib/components/SignUp.svelte b/src/lib/components/SignUp.svelte
index bc2053a..a4b435f 100644
--- a/src/lib/components/SignUp.svelte
+++ b/src/lib/components/SignUp.svelte
@@ -1,10 +1,15 @@
+
+
diff --git a/src/routes/profile/+page.svelte b/src/routes/profile/+page.svelte
deleted file mode 100644
index 6b9d79f..0000000
--- a/src/routes/profile/+page.svelte
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
- {#if session?.user}
- {session?.user?.email}
- {:else}
-
Please log in to view this page
- {/if}
-
From 540352657cfbe2a092b7ef0e26b2977ee25499cf Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 19:35:23 +0100
Subject: [PATCH 10/22] feat(#14): Add stripe webhooks
---
src/routes/api/webhook/stripe/+page.svelte | 0
src/routes/api/webhook/stripe/+server.ts | 96 ++++++++++++++++++++++
2 files changed, 96 insertions(+)
create mode 100644 src/routes/api/webhook/stripe/+page.svelte
create mode 100644 src/routes/api/webhook/stripe/+server.ts
diff --git a/src/routes/api/webhook/stripe/+page.svelte b/src/routes/api/webhook/stripe/+page.svelte
new file mode 100644
index 0000000..e69de29
diff --git a/src/routes/api/webhook/stripe/+server.ts b/src/routes/api/webhook/stripe/+server.ts
new file mode 100644
index 0000000..9b7b70b
--- /dev/null
+++ b/src/routes/api/webhook/stripe/+server.ts
@@ -0,0 +1,96 @@
+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:
+ throw new Error(`Unhandled event type ${event.type}`);
+ }
+
+ // Return a response to acknowledge receipt of the event
+ return {
+ status: 200,
+ body: { received: true }
+ };
+ } catch (err) {
+ console.log(`Webhook Error: ${err.message}`);
+ return {
+ status: 400,
+ body: `Webhook Error: ${err.message}`
+ };
+ }
+};
From d30d6181cff9ec7c3562dc22016ebc539944ac45 Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 19:54:09 +0100
Subject: [PATCH 11/22] docs(#14): Add stripe docs
---
docs/stripe-setup.md | 35 +++++++++++++++++++++++++++++++++++
1 file changed, 35 insertions(+)
create mode 100644 docs/stripe-setup.md
diff --git a/docs/stripe-setup.md b/docs/stripe-setup.md
new file mode 100644
index 0000000..df68b97
--- /dev/null
+++ b/docs/stripe-setup.md
@@ -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.
From 8634e56bd13a39a6a23445b07ab8fa64e341b4a7 Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 19:54:23 +0100
Subject: [PATCH 12/22] docs(#14): Add links to database and stripe docs from
readme
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index 2770d3a..a6e67eb 100644
--- a/README.md
+++ b/README.md
@@ -42,3 +42,7 @@ In its current configuration, the application can be easily linked to Netlify vi
## To Do
Please see the Issues tab for `enhancement` tagged issues.
+
+## [Database setup](/docs/database-setup.md)
+
+## [Stripe webhook event listening](/docs/stripe-setup.md)
From a10d22d1f85348620ec4f3d3b065f4fcbd7fa2dc Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 20:05:22 +0100
Subject: [PATCH 13/22] chore(#14): Strongly type supabase client
---
src/app.d.ts | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/app.d.ts b/src/app.d.ts
index 5ca5b29..a604567 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -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;
+ 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 {}
}
From 2fe426b3f42e564400d794c597a4b4f5e5bd88fe Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 20:13:19 +0100
Subject: [PATCH 14/22] chore(#14): Add initial db migration
---
.../20240704190621_initial_migration.sql | 159 ++++++++++++++++++
1 file changed, 159 insertions(+)
create mode 100644 supabase/migrations/20240704190621_initial_migration.sql
diff --git a/supabase/migrations/20240704190621_initial_migration.sql b/supabase/migrations/20240704190621_initial_migration.sql
new file mode 100644
index 0000000..240c5a0
--- /dev/null
+++ b/supabase/migrations/20240704190621_initial_migration.sql
@@ -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;
\ No newline at end of file
From 48aaee744fdbe17c4257a1797c74cc7641cdb2a6 Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 20:13:46 +0100
Subject: [PATCH 15/22] docs(#14): Add local db and migrations
---
docs/database-setup.md | 54 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 53 insertions(+), 1 deletion(-)
diff --git a/docs/database-setup.md b/docs/database-setup.md
index 7893d1b..da1045c 100644
--- a/docs/database-setup.md
+++ b/docs/database-setup.md
@@ -1,6 +1,10 @@
# 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. 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.
+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
/**
@@ -164,6 +168,8 @@ 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`
@@ -177,3 +183,49 @@ Replace `$PROJECT_REF` with your project reference:
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 `
+
+ - 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
+
+ `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 `
From 14992c37985a8969237e3e216305ae7d88e3f85c Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Thu, 4 Jul 2024 20:20:00 +0100
Subject: [PATCH 16/22] docs(*): Add to do item
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index a6e67eb..f5f1233 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,8 @@ 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)
From 337bc648ca5730c26f6150dae2db0c7c6e33b32c Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Fri, 5 Jul 2024 15:07:22 +0100
Subject: [PATCH 17/22] fix(#14): Fix accessing private env var
---
src/lib/utils/supabase/admin.ts | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts
index 71a9439..a5a53eb 100644
--- a/src/lib/utils/supabase/admin.ts
+++ b/src/lib/utils/supabase/admin.ts
@@ -1,11 +1,8 @@
import { createClient } from '@supabase/supabase-js';
import stripe from 'stripe';
import type { Database, Tables, TablesInsert } from '$types/supabase';
-import {
- PUBLIC_SUPABASE_URL,
- SUPABASE_SERVICE_ROLE_KEY,
- PUBLIC_STRIPE_SECRET_KEY
-} from '$env/static/public';
+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.
From 103ee16cecf9c5ff2cd446d2cefcf68a95931fe3 Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Fri, 5 Jul 2024 15:07:33 +0100
Subject: [PATCH 18/22] fix(#14): Fix invalid response
---
src/routes/api/webhook/stripe/+server.ts | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/routes/api/webhook/stripe/+server.ts b/src/routes/api/webhook/stripe/+server.ts
index 9b7b70b..a74d299 100644
--- a/src/routes/api/webhook/stripe/+server.ts
+++ b/src/routes/api/webhook/stripe/+server.ts
@@ -82,10 +82,9 @@ export const POST: RequestHandler = async ({ request }) => {
}
// Return a response to acknowledge receipt of the event
- return {
- status: 200,
- body: { received: true }
- };
+ return new Response(JSON.stringify({ received: true }), {
+ status: 200
+ });
} catch (err) {
console.log(`Webhook Error: ${err.message}`);
return {
From 1f0ff5bc5c24c43e52449dfa7a4ddd5e9707a05f Mon Sep 17 00:00:00 2001
From: OllyNicholass
Date: Fri, 5 Jul 2024 15:11:43 +0100
Subject: [PATCH 19/22] chore(#14): update schema type def
---
types/supabase.d.ts | 544 ++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 527 insertions(+), 17 deletions(-)
diff --git a/types/supabase.d.ts b/types/supabase.d.ts
index a0cfd54..d364511 100644
--- a/types/supabase.d.ts
+++ b/types/supabase.d.ts
@@ -7,36 +7,227 @@ export type Json =
| 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: {
- profiles: {
+ customers: {
Row: {
- avatar_url: string | null
- full_name: string | null
id: string
- updated_at: string | null
- username: string | null
- website: string | null
+ stripe_customer_id: string | null
}
Insert: {
- avatar_url?: string | null
- full_name?: string | null
id: string
- updated_at?: string | null
- username?: string | null
- website?: string | null
+ stripe_customer_id?: string | null
}
Update: {
- avatar_url?: string | null
- full_name?: string | null
id?: string
- updated_at?: string | null
- username?: string | null
- website?: string | null
+ stripe_customer_id?: string | null
}
Relationships: [
{
- foreignKeyName: "profiles_id_fkey"
+ 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"
@@ -51,6 +242,324 @@ export type Database = {
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
+ 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
+ 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
}
@@ -141,3 +650,4 @@ export type Enums<
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
? PublicSchema["Enums"][PublicEnumNameOrOptions]
: never
+
From c32d62d4203d71e2ded0e5e6310f46673e402958 Mon Sep 17 00:00:00 2001
From: OllyNicholass
Date: Fri, 5 Jul 2024 15:12:26 +0100
Subject: [PATCH 20/22] docs(#14): add linked db command
---
docs/database-setup.md | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/docs/database-setup.md b/docs/database-setup.md
index da1045c..0e7e4a4 100644
--- a/docs/database-setup.md
+++ b/docs/database-setup.md
@@ -220,7 +220,7 @@ There are two options to create a migration file:
- Reset the DB and apply the latest migrations
- `supabase db reset`
+ `npx supabase db reset`
2. [Generate SQL based on differences to the schema](https://supabase.com/docs/guides/cli/local-development#diffing-changes)
@@ -229,3 +229,11 @@ There are two options to create a migration file:
- Generate the migration file containing the SQL with the differences in the schema
`npx supabase db diff -f `
+
+## Applying migrations
+
+Reset database in cloud
+
+> Prerequisite login using `npx supabase login`
+
+`npx supabase db reset --linked`
From 67fcf8ea382c35b9d6f2cdf6a6a80e5f1913fa37 Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Fri, 5 Jul 2024 15:37:08 +0100
Subject: [PATCH 21/22] fix(#14): Replace throw with error log
---
src/routes/api/webhook/stripe/+server.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/api/webhook/stripe/+server.ts b/src/routes/api/webhook/stripe/+server.ts
index a74d299..7370213 100644
--- a/src/routes/api/webhook/stripe/+server.ts
+++ b/src/routes/api/webhook/stripe/+server.ts
@@ -78,7 +78,7 @@ export const POST: RequestHandler = async ({ request }) => {
break;
}
default:
- throw new Error(`Unhandled event type ${event.type}`);
+ console.error(`Unhandled event type ${event.type}`);
}
// Return a response to acknowledge receipt of the event
From 3fa3ace4ee3b897514468a0e5bd28d589ba0df4b Mon Sep 17 00:00:00 2001
From: OllyNicholass
Date: Fri, 5 Jul 2024 16:07:06 +0100
Subject: [PATCH 22/22] fix(#14): remove empty pages
---
src/routes/api/checkout/+page.svelte | 0
src/routes/api/checkout/status/+page.svelte | 0
src/routes/api/somebackendfunction/+page.svelte | 0
src/routes/api/webhook/stripe/+page.svelte | 0
src/routes/auth/confirm/+page.svelte | 0
src/routes/products/prices/[priceId]/+page.svelte | 0
6 files changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 src/routes/api/checkout/+page.svelte
delete mode 100644 src/routes/api/checkout/status/+page.svelte
delete mode 100644 src/routes/api/somebackendfunction/+page.svelte
delete mode 100644 src/routes/api/webhook/stripe/+page.svelte
delete mode 100644 src/routes/auth/confirm/+page.svelte
delete mode 100644 src/routes/products/prices/[priceId]/+page.svelte
diff --git a/src/routes/api/checkout/+page.svelte b/src/routes/api/checkout/+page.svelte
deleted file mode 100644
index e69de29..0000000
diff --git a/src/routes/api/checkout/status/+page.svelte b/src/routes/api/checkout/status/+page.svelte
deleted file mode 100644
index e69de29..0000000
diff --git a/src/routes/api/somebackendfunction/+page.svelte b/src/routes/api/somebackendfunction/+page.svelte
deleted file mode 100644
index e69de29..0000000
diff --git a/src/routes/api/webhook/stripe/+page.svelte b/src/routes/api/webhook/stripe/+page.svelte
deleted file mode 100644
index e69de29..0000000
diff --git a/src/routes/auth/confirm/+page.svelte b/src/routes/auth/confirm/+page.svelte
deleted file mode 100644
index e69de29..0000000
diff --git a/src/routes/products/prices/[priceId]/+page.svelte b/src/routes/products/prices/[priceId]/+page.svelte
deleted file mode 100644
index e69de29..0000000