Payments
How payments and billing are handled through Stripe.
@repo/payments wraps Stripe for payments and billing.
Stripe is an optional integration. If STRIPE_SECRET_KEY is not set, the stripe export will be undefined and payment webhooks will be skipped.
In-App Purchases
You can use Stripe anywhere in your app by importing the stripe object like so:
import { stripe } from '@repo/payments';
// ...
await stripe?.prices.list();Webhooks
apps/api carries a handler for payment events at app/webhooks/payments/route.ts. See inbound payment webhooks.
Anti-Fraud
As your app grows, you will inevitably encounter credit card fraud. Stripe Radar is enabled by default if you integrate payments using their SDK as described above. This provides a set of tools to help you detect and prevent fraud.
Stripe Radar supports more advanced anti-fraud features if the Stripe JS script is embedded on every page load. That is not wired up here. To add it:
Edit apps/app/app/layout.tsx and add <Script src="https://js.stripe.com/v3/" /> after the opening <html> tag and before the opening <body> tag. You will also need to add import Script from 'next/script'
import '@repo/design-system/styles/globals.css';
import { DesignSystemProvider } from '@repo/design-system';
import { fonts } from '@repo/design-system/lib/fonts';
import type { ReactNode } from 'react';
import Script from 'next/script';
type RootLayoutProperties = {
readonly children: ReactNode;
};
const RootLayout = ({ children }: RootLayoutProperties) => (
<html lang="en" className={fonts} suppressHydrationWarning>
<Script src="https://js.stripe.com/v3/" />
<body>
<DesignSystemProvider>{children}</DesignSystemProvider>
</body>
</html>
);
export default RootLayout;Add the same script to the website in apps/web/app/layout.tsx.
Prevent common fraud patterns by using Arcjet IP address analysis to block requests from VPNs and proxies. These are commonly used by fraudsters to hide their location, but have legitimate uses as well so are not blocked by default. You could simply block these users, or you could adjust the checkout process to require approval before processing their payment.
For example, in apps/app/app/(authenticated)/layout.tsx you could add this after the call to aj.protect():
import { redirect } from 'next/navigation';
// ...
if (
decision.ip.isHosting() ||
decision.ip.isVpn() ||
decision.ip.isProxy() ||
decision.ip.isRelay()
) {
// The IP is from a hosting provider, VPN, or proxy. We can check the name
// of the service and customize the response
if (decision.ip.hasService()) {
if (decision.ip.service !== 'Apple Private Relay') {
// We trust Apple Private Relay because it requires an active iCloud
// subscription, so deny all other VPNs
redirect('/vpn-blocked');
}
} else {
// The service name is not available, but we still think it's a VPN
redirect('/vpn-blocked');
}
}In this case we are providing a friendly redirect to a page that explains why the user is being blocked (which you would need to create). You could also return a more generic error message. See the Arcjet documentation for more advanced examples.