Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
809e982
Stripe only changes
FranjoMindek Sep 17, 2025
3df17a2
changes
FranjoMindek Sep 17, 2025
d0a2e15
further improvements
FranjoMindek Sep 17, 2025
1560833
remove typ catch
FranjoMindek Sep 21, 2025
fec25ab
split payment details funcs
FranjoMindek Sep 21, 2025
05b0fad
add awaits
FranjoMindek Sep 21, 2025
91e2f0d
name
FranjoMindek Sep 21, 2025
de70c30
Merge branch 'main' into franjo/refactor-stripe-payment-provider
FranjoMindek Sep 21, 2025
0cbe24c
formatting
FranjoMindek Sep 21, 2025
5dce0da
fix format
FranjoMindek Sep 21, 2025
4d6a64f
update
FranjoMindek Sep 21, 2025
1541759
map to switch
FranjoMindek Sep 21, 2025
dddc128
wording
FranjoMindek Sep 21, 2025
d7b5f8f
fix typo
FranjoMindek Sep 22, 2025
b764193
Merge branch 'main' into franjo/refactor-stripe-payment-provider
FranjoMindek Sep 24, 2025
d6a608a
format
FranjoMindek Sep 24, 2025
346da0f
fix diffs?
FranjoMindek Sep 24, 2025
4d9fb58
Merge remote-tracking branch 'origin/main' into franjo/refactor-strip…
FranjoMindek Sep 24, 2025
8371bd4
docs: update LLM files after documentation changes
FranjoMindek Sep 24, 2025
e5caba8
comments
FranjoMindek Oct 1, 2025
2e5ea07
wording
FranjoMindek Oct 1, 2025
f09865f
wording
FranjoMindek Oct 1, 2025
b4f80d3
wording
FranjoMindek Oct 1, 2025
2c82386
Merge branch 'main' into franjo/refactor-stripe-payment-provider
FranjoMindek Oct 1, 2025
34209ae
fix typo
FranjoMindek Oct 1, 2025
f288f9b
to 204
FranjoMindek Oct 1, 2025
0bd0b73
Merge remote-tracking branch 'origin/main' into franjo/refactor-strip…
FranjoMindek Oct 9, 2025
375cf1a
Merge remote-tracking branch 'origin/main' into franjo/refactor-strip…
FranjoMindek Oct 11, 2025
4673ec6
update env
FranjoMindek Oct 11, 2025
fb0ff94
formatting
FranjoMindek Oct 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
STRIPE_API_KEY: ${{ secrets.STRIPE_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
STRIPE_CUSTOMER_PORTAL_URL: https://billing.stripe.com/p/login/test_8wM8x17JN7DT4zC000
PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID: ${{ secrets.STRIPE_HOBBY_SUBSCRIPTION_PRICE_ID }}
PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID: ${{ secrets.STRIPE_PRO_SUBSCRIPTION_PRICE_ID }}
PAYMENTS_CREDITS_10_PLAN_ID: ${{ secrets.STRIPE_CREDITS_PRICE_ID }}
Expand Down
15 changes: 8 additions & 7 deletions opensaas-sh/app_diff/src/analytics/stats.ts.diff
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
--- template/app/src/analytics/stats.ts
+++ opensaas-sh/app/src/analytics/stats.ts
@@ -2,11 +2,9 @@
import { type DailyStatsJob } from 'wasp/server/jobs';
import Stripe from 'stripe';
import { stripe } from '../payment/stripe/stripeClient';
@@ -1,12 +1,10 @@
-import { listOrders } from '@lemonsqueezy/lemonsqueezy.js';
import Stripe from 'stripe';
import { type DailyStats } from 'wasp/entities';
import { type DailyStatsJob } from 'wasp/server/jobs';
+import { SubscriptionStatus } from '../payment/plans';
import { stripeClient } from '../payment/stripe/stripeClient';
import { getDailyPageViews, getSources } from './providers/plausibleAnalyticsUtils';
-// import { getDailyPageViews, getSources } from './providers/googleAnalyticsUtils';
// import { getDailyPageViews, getSources } from './providers/googleAnalyticsUtils';
-import { paymentProcessor } from '../payment/paymentProcessor';
import { SubscriptionStatus } from '../payment/plans';
+// import { getDailyPageViews, getSources } from './providers/googleAnalyticsUtils';
-import { SubscriptionStatus } from '../payment/plans';

export type DailyStatsProps = { dailyStats?: DailyStats; weeklyStats?: DailyStats[]; isLoading?: boolean };

Expand Down
21 changes: 13 additions & 8 deletions opensaas-sh/app_diff/src/payment/stripe/paymentDetails.ts.diff
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
--- template/app/src/payment/stripe/paymentDetails.ts
+++ opensaas-sh/app/src/payment/stripe/paymentDetails.ts
@@ -14,10 +14,10 @@
) => {
@@ -26,7 +26,7 @@
) {
return userDelegate.update({
where: {
- paymentProcessorUserId: userStripeId
+ stripeId: userStripeId
- paymentProcessorUserId: customerId,
+ stripeId: customerId,
},
data: {
- paymentProcessorUserId: userStripeId,
+ stripeId: userStripeId,
subscriptionPlan,
subscriptionStatus,
datePaid,
@@ -48,7 +48,7 @@
) {
return userDelegate.update({
where: {
- paymentProcessorUserId: customerId,
+ stripeId: customerId,
},
data: {
subscriptionPlan: paymentPlanId,
14 changes: 7 additions & 7 deletions opensaas-sh/app_diff/src/payment/stripe/paymentProcessor.ts.diff
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
--- template/app/src/payment/stripe/paymentProcessor.ts
+++ opensaas-sh/app/src/payment/stripe/paymentProcessor.ts
@@ -20,7 +20,7 @@
id: userId
@@ -24,7 +24,7 @@
id: userId,
},
data: {
- paymentProcessorUserId: customer.id
+ stripeId: customer.id
}
})
if (!stripeSession.url) throw new Error('Error creating Stripe Checkout Session');
- paymentProcessorUserId: customer.id,
+ stripeId: customer.id,
},
});

75 changes: 53 additions & 22 deletions opensaas-sh/blog/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ If you find this template useful, consider giving us [a star on GitHub](https://
## What's inside?

The template itself is built on top of some very powerful tools and frameworks, including:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hasn't been run in a while so more changes pop up.

- [Wasp](https://wasp.sh) - a full-stack React, NodeJS, Prisma framework with superpowers- [Astro](https://starlight.astro.build/) - Astro's lightweight "Starlight" template for documentation and blog
- [Wasp](https://wasp.sh) - a full-stack React, NodeJS, Prisma framework with superpowers
- [Astro](https://starlight.astro.build/) - Astro's lightweight "Starlight" template for documentation and blog
- [Stripe](https://stripe.com) or [Lemon Squeezy](https://lemonsqueezy.com/) - for products and payments
- [Plausible](https://plausible.io) or [Google](https://analytics.google.com/) Analytics
- [OpenAI](https://openai.com) - OpenAI API integrated into the app or [Replicate](https://replicate.com/) (coming soon )
Expand Down Expand Up @@ -64,7 +65,7 @@ If you prefer video tutorials, you can watch this walkthrough below which will g
### Pre-requisites

You must have Node.js (and NPM) installed on your machine and available in `PATH` to use Wasp.
Your version of Node.js must be >= 20.
Your version of Node.js must be >= 22.12.

To switch easily between Node.js versions, we recommend using [nvm](https://github.com/nvm-sh/nvm).

Expand Down Expand Up @@ -128,7 +129,7 @@ Once Rosetta is installed, you should be able to run Wasp without any issues.

### Windows

In order to use Wasp on Windows, you need to install WSL2 (Windows Subsystem for Linux) and a Linux distribution of your choice. We recommend using Ubuntu.
In order to use Wasp on Windows, you need to install WSL2 (Windows Subsystem for Linux) and a Linux distribution of your choice. We recommend using Ubuntu.

**You can refer to this [article](https://wasp.sh/blog/2023/11/21/guide-windows-development-wasp-wsl) for a step by step guide to using Wasp in the WSL environment.** If you need further help, reach out to us on [Discord](https://discord.gg/rzdnErX).

Expand Down Expand Up @@ -173,7 +174,7 @@ curl -sSL https://get.wasp.sh/installer.sh | sh
</summary>
If you are using WSL2, make sure that your Wasp project is not on the Windows file system, <b>but instead on the Linux file system</b>. Otherwise, Wasp won't be able to detect file changes, due to this <a href='https://github.com/microsoft/WSL/issues/4739'>issue in WSL2</a>.
</details>
:::
:::

### Finalize Installation

Expand Down Expand Up @@ -874,7 +875,7 @@ To create a Google OAuth app and get your Google API keys, follow the instructio

To create a GitHub OAuth app and get your GitHub API keys, follow the instructions in [Wasp's GitHub Auth docs](https://wasp.sh/docs/auth/social-auth/github#3-creating-a-github-oauth-app).

To create a Discord OAuth app and get your Discord API keys, follow the instructions in [Wasp's Discord Auth docs](docs/auth/social-auth/google#3-creating-a-google-oauth-app)
To create a Discord OAuth app and get your Discord API keys, follow the instructions in [Wasp's Discord Auth docs](https://wasp.sh/docs/auth/social-auth/discord#3-creating-a-discord-app)

Again, Wasp will take care of the rest and update your AuthUI components accordingly.

Expand Down Expand Up @@ -1287,13 +1288,7 @@ export const stripe = new Stripe(process.env.STRIPE_KEY!, {
2. click on `+ add endpoint`
3. enter your endpoint url, which will be the url of your deployed server + `/payments-webhook`, e.g. `https://open-saas-wasp-sh-server.fly.dev/payments-webhook`

4. select the events you want to listen to. These should be the same events you're consuming in your webhook. For example, if you haven't added any additional events to the webhook and are using the defaults that came with this template, then you'll need to add:
<br/>- `account.updated`
<br/>- `checkout.session.completed`
<br/>- `customer.subscription.deleted`
<br/>- `customer.subscription.updated`
<br/>- `invoice.paid`
<br/>- `payment_intent.succeeded`
4. select the events you want to listen to. These should be the same events you're consuming in your webhook which you can find listed in [`src/payment/stripe/webhookPayload.ts`](https://github.com/wasp-lang/open-saas/blob/main/template/app/src/payment/stripe/webhookPayload.ts):

5. after that, go to the webhook you just created and `reveal` the new signing secret.
6. add this secret to your deployed server's `STRIPE_WEBHOOK_SECRET=` environment variable. <br/>If you've deployed to Fly.io, you can do that easily with the following command:
Expand Down Expand Up @@ -1620,7 +1615,7 @@ export const paymentProcessor: PaymentProcessor = lemonSqueezyPaymentProcessor;
At this point, you can delete:
- the unused payment processor code within the `/src/payment/<unused-provider>` directory,
- any unused environment variables from `.env.server` (they will be prefixed with the name of the provider your are not using):
- e.g. `STRIPE_API_KEY`, `STRIPE_CUSTOMER_PORTAL_URL`, `LEMONSQUEEZY_API_KEY`, `LEMONSQUEEZY_WEBHOOK_SECRET`
- e.g. `STRIPE_API_KEY`, `LEMONSQUEEZY_API_KEY`
- Make sure to also uninstall the unused dependencies:
- `npm uninstall @lemonsqueezy/lemonsqueezy.js`
- or
Expand Down Expand Up @@ -1666,22 +1661,21 @@ To create a test product, go to the test products url [https://dashboard.stripe.

### Create a Test Customer

To create a test customer, go to the test customers url [https://dashboard.stripe.com/test/customers](https://dashboard.stripe.com/test/customers).
You can create a test customer directly in the [Stripe Dashboard](https://dashboard.stripe.com/test/customers).

- Click on the `Add a customer` button and fill in the relevant information for your test customer.
:::note
When filling in the test customer email address, use an address you have access to and will use when logging into your SaaS app. This is important because the email address is used to identify the customer when creating a subscription and allows you to manage your test user's payments/subscriptions via the test customer portal
:::

Alternatively, OpenSasS will automatically create a test customer the first time a user starts a checkout session.
This customer is linked to the email address associated with your app's user.

### Set up the Customer Portal

Go to https://dashboard.stripe.com/test/settings/billing/portal in the Stripe Dashboard and activate and copy the `Customer portal link`. Paste it in your `.env.server` file:
You can set up your customer portal in your [Stripe Dashboard](https://dashboard.stripe.com/test/settings/billing/portal).

```ts title=".env.server"
STRIPE_CUSTOMER_PORTAL_URL=<your-test-customer-portal-link>
```
By default, OpenSaas generates a unique customer portal link for each user on the back end.
If you'd rather provide a permanent link to the customer portal, activate and copy the `Customer portal link`.

If you'd like to give users the ability to switch between different plans, e.g. upgrade from a hobby to a pro subscription, go down to the `Subscriptions` dropdown and select `customers can switch plans`.
If you'd like to give users the ability to switch between different plans, e.g., upgrade from a "Hobby" to a "Pro" subscription, go down to the `Subscriptions` dropdown and select `customers can switch plans`.

Then select the products you'd like them to be able to switch between.

Expand Down Expand Up @@ -2015,6 +2009,43 @@ The method you choose is up to you and will largely depend on the complexity of

---

# Vibe Coding with Open SaaS

If you're looking to use AI to help build (or "vibe code") your SaaS app, this guide is for you.

## Coding with AI, Open SaaS, & Wasp

Wasp is particularly well suited to coding with AI due to its central config file which gives LLMs context about the entire full-stack app, and its ability to manage boilerplate code so AI doesn't have to.

Regardless, there are still some shortcomings to using AI to code with Wasp, as well as a learning curve to using it effectively.

Luckily, we did the work for you and put together a bunch of resources to help you use Wasp & Open SaaS with AI as effectively as possible.

### AI Resources in the Template

The template comes with:
- A full set of rules files, `app/.cursor/rules`, to be used with Cursor or adapted to your coding tool of choice (Windsurf, Claude Code, etc.).
- A set of example prompts, `app/.cursor/example-prompts.md`, to help you get started.

### LLM-Friendly Documentation

We've also created a bunch of LLM-friendly documentation:
- [Open SaaS Docs - LLMs.txt](https://docs.opensaas.sh/llms.txt) - Links to the raw text docs.
- **[Open SaaS Docs - LLMs-full.txt](https://docs.opensaas.sh/llms-full.txt) - Complete docs as one text file.**
- Coming Soon! ~~[Wasp Docs - LLMs.txt](https://wasp.sh/llms.txt)~~ - Links to the raw text docs.
- Coming Soon! ~~[Wasp Docs - LLMs-full.txt](https://wasp.sh/llms-full.txt)~~ - Complete docs as one text file.

Add these to your AI-assisted IDE settings so you can easily reference them in your chat sessions with the LLM.
**In most cases, you'll want to pass the `llms-full.txt` to the LLM and ask it to help you with a specific task.**

### More AI-assisted Coding Learning Resources

Here's a list of articles and tutorials we've made:
- [3hr YouTube tutorial: Vibe Coding a Personal Finance App w/ Wasp & Cursor](https://www.youtube.com/watch?v=WYzEROo7reY)
- [Article: A Structured Workflow for "Vibe Coding" Full-Stack Apps](https://dev.to/wasp/a-structured-workflow-for-vibe-coding-full-stack-apps-352l)

---

# Admin Dashboard

This is a reference on how the Admin dashboard, available at `/admin`, is set up.
Expand Down
1 change: 1 addition & 0 deletions opensaas-sh/blog/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
- [SEO](https://raw.githubusercontent.com/wasp-lang/open-saas/main/opensaas-sh/blog/src/content/docs/guides/seo.mdx)
- [Tests](https://raw.githubusercontent.com/wasp-lang/open-saas/main/opensaas-sh/blog/src/content/docs/guides/tests.md)
- [How (Not) to Update Your Open SaaS App](https://raw.githubusercontent.com/wasp-lang/open-saas/main/opensaas-sh/blog/src/content/docs/guides/updating-opensaas.md)
- [Vibe Coding with Open SaaS](https://raw.githubusercontent.com/wasp-lang/open-saas/main/opensaas-sh/blog/src/content/docs/guides/vibe-coding.mdx)
- [Admin Dashboard](https://raw.githubusercontent.com/wasp-lang/open-saas/main/opensaas-sh/blog/src/content/docs/general/admin-dashboard.mdx)
- [User Overview](https://raw.githubusercontent.com/wasp-lang/open-saas/main/opensaas-sh/blog/src/content/docs/general/user-overview.md)
19 changes: 9 additions & 10 deletions opensaas-sh/blog/src/content/docs/guides/payments-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const paymentProcessor: PaymentProcessor = lemonSqueezyPaymentProcessor;
At this point, you can delete:
- the unused payment processor code within the `/src/payment/<unused-provider>` directory,
- any unused environment variables from `.env.server` (they will be prefixed with the name of the provider your are not using):
- e.g. `STRIPE_API_KEY`, `STRIPE_CUSTOMER_PORTAL_URL`, `LEMONSQUEEZY_API_KEY`, `LEMONSQUEEZY_WEBHOOK_SECRET`
- e.g. `STRIPE_API_KEY`, `LEMONSQUEEZY_API_KEY`
- Make sure to also uninstall the unused dependencies:
- `npm uninstall @lemonsqueezy/lemonsqueezy.js`
- or
Expand Down Expand Up @@ -95,22 +95,21 @@ To create a test product, go to the test products url [https://dashboard.stripe.

### Create a Test Customer

To create a test customer, go to the test customers url [https://dashboard.stripe.com/test/customers](https://dashboard.stripe.com/test/customers).
You can create a test customer directly in the [Stripe Dashboard](https://dashboard.stripe.com/test/customers).

- Click on the `Add a customer` button and fill in the relevant information for your test customer.
:::note
When filling in the test customer email address, use an address you have access to and will use when logging into your SaaS app. This is important because the email address is used to identify the customer when creating a subscription and allows you to manage your test user's payments/subscriptions via the test customer portal
:::

Alternatively, OpenSasS will automatically create a test customer the first time a user starts a checkout session.
This customer is linked to the email address associated with your app's user.

### Set up the Customer Portal

Go to https://dashboard.stripe.com/test/settings/billing/portal in the Stripe Dashboard and activate and copy the `Customer portal link`. Paste it in your `.env.server` file:
You can set up your customer portal in your [Stripe Dashboard](https://dashboard.stripe.com/test/settings/billing/portal).

```ts title=".env.server"
STRIPE_CUSTOMER_PORTAL_URL=<your-test-customer-portal-link>
```
By default, OpenSaas generates a unique customer portal link for each user on the back end.
If you'd rather provide a permanent link to the customer portal, activate and copy the `Customer portal link`.

If you'd like to give users the ability to switch between different plans, e.g. upgrade from a hobby to a pro subscription, go down to the `Subscriptions` dropdown and select `customers can switch plans`.
If you'd like to give users the ability to switch between different plans, e.g., upgrade from a "Hobby" to a "Pro" subscription, go down to the `Subscriptions` dropdown and select `customers can switch plans`.

<Image src={switchPlans} alt="switch plans" loading="lazy" />

Expand Down
2 changes: 0 additions & 2 deletions template/app/.env.server.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
STRIPE_API_KEY=sk_test_...
# After downloading starting the stripe cli (https://stripe.com/docs/stripe-cli) with `stripe listen --forward-to localhost:3001/payments-webhook` it will output your signing secret
STRIPE_WEBHOOK_SECRET=whsec_...
# You can find your Stripe customer portal URL in the Stripe Dashboard under the 'Customer Portal' settings.
STRIPE_CUSTOMER_PORTAL_URL=https://billing.stripe.com/...

# For testing, create a new store in test mode on https://lemonsqueezy.com
LEMONSQUEEZY_API_KEY=eyJ...
Expand Down
8 changes: 4 additions & 4 deletions template/app/src/analytics/stats.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { listOrders } from '@lemonsqueezy/lemonsqueezy.js';
import Stripe from 'stripe';
import { type DailyStats } from 'wasp/entities';
import { type DailyStatsJob } from 'wasp/server/jobs';
import Stripe from 'stripe';
import { stripe } from '../payment/stripe/stripeClient';
import { listOrders } from '@lemonsqueezy/lemonsqueezy.js';
import { stripeClient } from '../payment/stripe/stripeClient';
import { getDailyPageViews, getSources } from './providers/plausibleAnalyticsUtils';
// import { getDailyPageViews, getSources } from './providers/googleAnalyticsUtils';
import { paymentProcessor } from '../payment/paymentProcessor';
Expand Down Expand Up @@ -144,7 +144,7 @@ async function fetchTotalStripeRevenue() {

let hasMore = true;
while (hasMore) {
const balanceTransactions = await stripe.balanceTransactions.list(params);
const balanceTransactions = await stripeClient.balanceTransactions.list(params);

for (const transaction of balanceTransactions.data) {
if (transaction.type === 'charge') {
Expand Down
11 changes: 7 additions & 4 deletions template/app/src/payment/plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ export enum PaymentPlanId {
}

export interface PaymentPlan {
// Returns the id under which this payment plan is identified on your payment processor.
// E.g. this might be price id on Stripe, or variant id on LemonSqueezy.
/**
* Returns the id under which this payment plan is identified on your payment processor.
*
* E.g. price id on Stripe, or variant id on LemonSqueezy.
*/
getPaymentProcessorPlanId: () => string;
effect: PaymentPlanEffect;
}

export type PaymentPlanEffect = { kind: 'subscription' } | { kind: 'credits'; amount: number };

export const paymentPlans: Record<PaymentPlanId, PaymentPlan> = {
export const paymentPlans = {
[PaymentPlanId.Hobby]: {
getPaymentProcessorPlanId: () => requireNodeEnvVar('PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID'),
effect: { kind: 'subscription' },
Expand All @@ -35,7 +38,7 @@ export const paymentPlans: Record<PaymentPlanId, PaymentPlan> = {
getPaymentProcessorPlanId: () => requireNodeEnvVar('PAYMENTS_CREDITS_10_PLAN_ID'),
effect: { kind: 'credits', amount: 10 },
},
};
} as const satisfies Record<PaymentPlanId, PaymentPlan>;
Copy link
Contributor Author

@FranjoMindek FranjoMindek Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows us to do e.g. paymentPlans.credits10.effect.amount which would throw before.


export function prettyPaymentPlanName(planId: PaymentPlanId): string {
const planToName: Record<PaymentPlanId, string> = {
Expand Down
Loading