From 5588d418cdb473474b31ae3a56a126e4d9018ab3 Mon Sep 17 00:00:00 2001 From: audrzejq <31422031+audrzejq@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:03:17 +0200 Subject: [PATCH 1/7] new example with clerk auth --- .../.env.example | 15 + .../.github/workflows/lint.yml | 26 + .../.github/workflows/playwright.yml | 58 + .../.github/workflows/pull-upstream.yml | 39 + .../.gitignore | 27 + .../.oxlintrc.json | 14 + .../CHANGELOG.md | 105 ++ .../chatbot-with-billing-with-clerk/README.md | 73 + .../app/(chat)/actions.ts | 50 + .../app/(chat)/api/chat/route.ts | 33 + .../app/(chat)/api/history/route.ts | 58 + .../app/(chat)/chat/[id]/page.tsx | 27 + .../app/(chat)/chat/layout.tsx | 7 + .../app/(chat)/chat/page.tsx | 9 + .../app/(chat)/db-actions.ts | 29 + .../app/(chat)/layout.tsx | 44 + .../app/(chat)/opengraph-image.png | Bin 0 -> 14418 bytes .../app/(chat)/page.tsx | 7 + .../app/(chat)/twitter-image.png | Bin 0 -> 52049 bytes .../app/favicon.ico | Bin 0 -> 15406 bytes .../app/globals.css | 500 +++++++ .../app/layout.tsx | 84 ++ .../app/onboarding/page.tsx | 16 + .../app/sign-in/[[...sign-in]]/page.tsx | 9 + .../app/sign-up/[[...sign-up]]/page.tsx | 9 + .../components.json | 20 + .../components/chat/app-sidebar.tsx | 207 +++ .../components/chat/chat-header.tsx | 76 ++ .../components/chat/icons.tsx | 1213 +++++++++++++++++ .../components/chat/preview.tsx | 59 + .../components/chat/shell.tsx | 128 ++ .../components/chat/sidebar-history-item.tsx | 124 ++ .../components/chat/sidebar-history.tsx | 372 +++++ .../components/chat/sidebar-user-nav.tsx | 27 + .../components/chat/sign-in-button.tsx | 45 + .../components/chat/submit-button.tsx | 38 + .../components/chat/toast.tsx | 75 + .../components/chat/visibility-selector.tsx | 111 ++ .../components/theme-provider.tsx | 8 + .../components/ui/alert-dialog.tsx | 199 +++ .../components/ui/badge.tsx | 49 + .../components/ui/button-group.tsx | 83 ++ .../components/ui/button.tsx | 65 + .../components/ui/collapsible.tsx | 33 + .../components/ui/command.tsx | 191 +++ .../components/ui/dialog.tsx | 164 +++ .../components/ui/dropdown-menu.tsx | 273 ++++ .../components/ui/hover-card.tsx | 44 + .../components/ui/input-group.tsx | 155 +++ .../components/ui/input.tsx | 19 + .../components/ui/label.tsx | 24 + .../components/ui/popover.tsx | 49 + .../components/ui/scroll-area.tsx | 55 + .../components/ui/select.tsx | 198 +++ .../components/ui/separator.tsx | 28 + .../components/ui/sheet.tsx | 143 ++ .../components/ui/sidebar.tsx | 717 ++++++++++ .../components/ui/skeleton.tsx | 13 + .../components/ui/spinner.tsx | 15 + .../components/ui/textarea.tsx | 18 + .../components/ui/tooltip.tsx | 57 + .../components/usage/usage-content.tsx | 80 ++ .../drizzle.config.ts | 15 + .../hooks/use-chat-visibility.ts | 55 + .../hooks/use-mobile.ts | 21 + .../instrumentation-client.ts | 10 + .../instrumentation.ts | 5 + .../lib/ai/chat-setup.ts | 4 + .../lib/ai/prompts.ts | 119 ++ .../lib/ai/tools/get-weather.ts | 78 ++ .../lib/constants.ts | 14 + .../lib/db/migrate.ts | 33 + .../lib/db/migrations/0000_initial.sql | 67 + .../lib/db/migrations/meta/_journal.json | 13 + .../lib/db/queries.ts | 262 ++++ .../lib/db/schema.ts | 136 ++ .../lib/db/utils.ts | 4 + .../lib/errors.ts | 137 ++ .../lib/polar-client.ts | 31 + .../lib/types.ts | 26 + .../lib/utils.ts | 88 ++ .../next.config.ts | 50 + .../package.json | 115 ++ .../playwright.config.ts | 103 ++ .../postcss.config.mjs | 8 + .../chatbot-with-billing-with-clerk/proxy.ts | 10 + .../public/images/demo-thumbnail.png | Bin 0 -> 23198 bytes .../images/mouth of the seine, monet.jpg | Bin 0 -> 33497 bytes .../public/preview.png | Bin 0 -> 39834 bytes .../tests/e2e/api.test.ts | 95 ++ .../tests/e2e/auth.test.ts | 31 + .../tests/e2e/chat.test.ts | 61 + .../tests/e2e/model-selector.test.ts | 70 + .../tests/fixtures.ts | 15 + .../tests/helpers.ts | 16 + .../tests/pages/chat.ts | 71 + .../tests/prompts/utils.ts | 34 + .../tsconfig.json | 35 + .../vercel-template.json | 10 + .../vercel.json | 3 + knip.json | 1 + pnpm-lock.yaml | 377 +++++ 102 files changed, 8637 insertions(+) create mode 100644 examples/chatbot-with-billing-with-clerk/.env.example create mode 100644 examples/chatbot-with-billing-with-clerk/.github/workflows/lint.yml create mode 100644 examples/chatbot-with-billing-with-clerk/.github/workflows/playwright.yml create mode 100644 examples/chatbot-with-billing-with-clerk/.github/workflows/pull-upstream.yml create mode 100644 examples/chatbot-with-billing-with-clerk/.gitignore create mode 100644 examples/chatbot-with-billing-with-clerk/.oxlintrc.json create mode 100644 examples/chatbot-with-billing-with-clerk/CHANGELOG.md create mode 100644 examples/chatbot-with-billing-with-clerk/README.md create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/actions.ts create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/api/chat/route.ts create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/api/history/route.ts create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/chat/[id]/page.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/chat/layout.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/chat/page.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/db-actions.ts create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/layout.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/opengraph-image.png create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/page.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/app/(chat)/twitter-image.png create mode 100644 examples/chatbot-with-billing-with-clerk/app/favicon.ico create mode 100644 examples/chatbot-with-billing-with-clerk/app/globals.css create mode 100644 examples/chatbot-with-billing-with-clerk/app/layout.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/app/onboarding/page.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/app/sign-in/[[...sign-in]]/page.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/app/sign-up/[[...sign-up]]/page.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components.json create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/app-sidebar.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/chat-header.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/icons.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/preview.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/shell.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/sidebar-history-item.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/sidebar-history.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/sidebar-user-nav.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/sign-in-button.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/submit-button.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/toast.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/chat/visibility-selector.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/theme-provider.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/alert-dialog.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/badge.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/button-group.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/button.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/collapsible.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/command.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/dialog.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/dropdown-menu.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/hover-card.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/input-group.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/input.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/label.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/popover.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/scroll-area.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/select.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/separator.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/sheet.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/sidebar.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/skeleton.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/spinner.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/textarea.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/ui/tooltip.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/components/usage/usage-content.tsx create mode 100644 examples/chatbot-with-billing-with-clerk/drizzle.config.ts create mode 100644 examples/chatbot-with-billing-with-clerk/hooks/use-chat-visibility.ts create mode 100644 examples/chatbot-with-billing-with-clerk/hooks/use-mobile.ts create mode 100644 examples/chatbot-with-billing-with-clerk/instrumentation-client.ts create mode 100644 examples/chatbot-with-billing-with-clerk/instrumentation.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/ai/chat-setup.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/ai/prompts.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/ai/tools/get-weather.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/constants.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/db/migrate.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/db/migrations/0000_initial.sql create mode 100644 examples/chatbot-with-billing-with-clerk/lib/db/migrations/meta/_journal.json create mode 100644 examples/chatbot-with-billing-with-clerk/lib/db/queries.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/db/schema.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/db/utils.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/errors.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/polar-client.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/types.ts create mode 100644 examples/chatbot-with-billing-with-clerk/lib/utils.ts create mode 100644 examples/chatbot-with-billing-with-clerk/next.config.ts create mode 100644 examples/chatbot-with-billing-with-clerk/package.json create mode 100644 examples/chatbot-with-billing-with-clerk/playwright.config.ts create mode 100644 examples/chatbot-with-billing-with-clerk/postcss.config.mjs create mode 100644 examples/chatbot-with-billing-with-clerk/proxy.ts create mode 100644 examples/chatbot-with-billing-with-clerk/public/images/demo-thumbnail.png create mode 100644 examples/chatbot-with-billing-with-clerk/public/images/mouth of the seine, monet.jpg create mode 100644 examples/chatbot-with-billing-with-clerk/public/preview.png create mode 100644 examples/chatbot-with-billing-with-clerk/tests/e2e/api.test.ts create mode 100644 examples/chatbot-with-billing-with-clerk/tests/e2e/auth.test.ts create mode 100644 examples/chatbot-with-billing-with-clerk/tests/e2e/chat.test.ts create mode 100644 examples/chatbot-with-billing-with-clerk/tests/e2e/model-selector.test.ts create mode 100644 examples/chatbot-with-billing-with-clerk/tests/fixtures.ts create mode 100644 examples/chatbot-with-billing-with-clerk/tests/helpers.ts create mode 100644 examples/chatbot-with-billing-with-clerk/tests/pages/chat.ts create mode 100644 examples/chatbot-with-billing-with-clerk/tests/prompts/utils.ts create mode 100644 examples/chatbot-with-billing-with-clerk/tsconfig.json create mode 100644 examples/chatbot-with-billing-with-clerk/vercel-template.json create mode 100644 examples/chatbot-with-billing-with-clerk/vercel.json diff --git a/examples/chatbot-with-billing-with-clerk/.env.example b/examples/chatbot-with-billing-with-clerk/.env.example new file mode 100644 index 00000000..57f8f9ed --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/.env.example @@ -0,0 +1,15 @@ +# Clerk keys — https://dashboard.clerk.com +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=**** +CLERK_SECRET_KEY=**** + +# required for non-vercel deployments, vercel uses OIDC automatically +# https://vercel.com/ai-gateway +AI_GATEWAY_API_KEY=**** + +# https://vercel.com/docs/postgres +POSTGRES_URL=**** + +# https://polar.sh — billing destination +# POLAR_SERVER can be 'sandbox' (default) or 'production' +POLAR_ACCESS_TOKEN=**** +POLAR_SERVER=sandbox diff --git a/examples/chatbot-with-billing-with-clerk/.github/workflows/lint.yml b/examples/chatbot-with-billing-with-clerk/.github/workflows/lint.yml new file mode 100644 index 00000000..9fd04890 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: Lint +on: + push: + +jobs: + build: + runs-on: ubuntu-22.04 + strategy: + matrix: + node-version: [20] + steps: + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.32.1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + - name: Run lint + run: pnpm run lint + - name: Run type check + run: pnpm run check-types diff --git a/examples/chatbot-with-billing-with-clerk/.github/workflows/playwright.yml b/examples/chatbot-with-billing-with-clerk/.github/workflows/playwright.yml new file mode 100644 index 00000000..faf9f4d0 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/.github/workflows/playwright.yml @@ -0,0 +1,58 @@ +name: Playwright Tests +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + test: + timeout-minutes: 30 + runs-on: ubuntu-latest + env: + AUTH_SECRET: ${{ secrets.OSS_AUTH_SECRET }} + AI_GATEWAY_API_KEY: ${{ secrets.OSS_AI_GATEWAY_API_KEY }} + POSTGRES_URL: ${{ secrets.OSS_POSTGRES_URL }} + POLAR_ACCESS_TOKEN: ${{ secrets.OSS_POLAR_ACCESS_TOKEN }} + POLAR_SERVER: 'sandbox' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 10.32.1 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Build application + run: pnpm run build + + - name: Cache Playwright browsers + uses: actions/cache@v3 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: pnpm run test:setup + + - name: Run Playwright tests + run: pnpm run test:e2e + + - uses: actions/upload-artifact@v4 + if: always() && !cancelled() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 diff --git a/examples/chatbot-with-billing-with-clerk/.github/workflows/pull-upstream.yml b/examples/chatbot-with-billing-with-clerk/.github/workflows/pull-upstream.yml new file mode 100644 index 00000000..6042f22e --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/.github/workflows/pull-upstream.yml @@ -0,0 +1,39 @@ +name: Pull Upstream Template + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + pull-and-update: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 9 + + - name: Run pull script + run: node scripts/pull-upstream.js + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + commit-message: 'chore: sync upstream template from narevai/ai-billing' + branch: chore/sync-upstream + delete-branch: true + title: 'chore: sync upstream template from narevai/ai-billing' + body: 'Automated sync of upstream template changes from narevai/ai-billing.' diff --git a/examples/chatbot-with-billing-with-clerk/.gitignore b/examples/chatbot-with-billing-with-clerk/.gitignore new file mode 100644 index 00000000..b24a32e0 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/.gitignore @@ -0,0 +1,27 @@ +.pnpm-store/ +node_modules +.pnp +.pnp.js +coverage +.next/ +out/ +build +.DS_Store +*.pem +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +.env.local +.env.development.local +.env.production.local +.turbo +.env +.vercel +.env*.local +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/* +next-env.d.ts +tsconfig.tsbuildinfo diff --git a/examples/chatbot-with-billing-with-clerk/.oxlintrc.json b/examples/chatbot-with-billing-with-clerk/.oxlintrc.json new file mode 100644 index 00000000..640a04b3 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/.oxlintrc.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "plugins": ["typescript"], + "categories": { + "correctness": "warn", + "suspicious": "warn" + }, + "rules": { + "eslint/no-shadow": "off", + "eslint/no-loss-of-precision": "off", + "eslint/no-unused-vars": "off" + }, + "ignorePatterns": ["**/dist/**", "**/coverage/**", "**/.next/**"] +} diff --git a/examples/chatbot-with-billing-with-clerk/CHANGELOG.md b/examples/chatbot-with-billing-with-clerk/CHANGELOG.md new file mode 100644 index 00000000..0c60de32 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/CHANGELOG.md @@ -0,0 +1,105 @@ +# chatbot-with-billing + +## 0.0.11 + +### Patch Changes + +- 1545485: fix(nexjs): make all dependencies mandatory +- Updated dependencies [ae6c4a1] +- Updated dependencies [1545485] + - @ai-billing/nextjs@0.0.11 + +## 0.0.10 + +### Patch Changes + +- Updated dependencies [faf939a] + - @ai-billing/anthropic@0.1.3 + - @ai-billing/nextjs@0.0.10 + - @ai-billing/openai@0.1.3 + - @ai-billing/polar@0.1.6 + - @ai-billing/core@0.1.3 + +## 0.0.9 + +### Patch Changes + +- d0e7dd3: fix(examples): rename examples +- Updated dependencies [bebd70b] +- Updated dependencies [0126081] + - @ai-billing/nextjs@0.0.9 + +## 0.0.9 + +### Patch Changes + +- Updated dependencies [d64b99c] + - @ai-billing/nextjs@0.0.8 + +## 0.0.8 + +### Patch Changes + +- Updated dependencies [dd822d2] + - @ai-billing/nextjs@0.0.7 + +## 0.0.7 + +### Patch Changes + +- 3cd4ed7: feat(nextjs): create chat subcomponents +- Updated dependencies [3cd4ed7] + - @ai-billing/nextjs@0.0.6 + +## 0.0.6 + +### Patch Changes + +- Updated dependencies [186a7fc] + - @ai-billing/nextjs@0.0.5 + +## 0.0.5 + +### Patch Changes + +- Updated dependencies [81775fc] +- Updated dependencies [804b53f] + - @ai-billing/nextjs@0.0.4 + - @ai-billing/gateway@0.1.3 + +## 0.0.4 + +### Patch Changes + +- b410651: fix(examples): fix sidebar buttons +- Updated dependencies [34ef850] +- Updated dependencies [8de3cd8] + - @ai-billing/nextjs@0.0.3 + - @ai-billing/polar@0.1.5 + +## 0.0.3 + +### Patch Changes + +- 56442fa: feat(examples): add usage and topup components to chatbot examples +- Updated dependencies [ddbcaba] + - @ai-billing/nextjs@0.0.2 + +## 0.0.2 + +### Patch Changes + +- 6ee669f: chore: update next to 16.2.5 +- 6711ca0: feat(examples): add generation cost and deploy to vercel button +- 46fde94: chore(examples): update next and mermaid deps +- 8de76c1: fix(chatbot-with-billing-polar): Simplify chatbot example, remove overhelming deps + +## 0.0.1 + +### Patch Changes + +- b4a72c8: docs(polar): add ai-billing chatbot with polar backend +- 7680ac0: docs(polar): adding automated template repo sync +- 4d8787f: docs(polar): add auto-update script for template +- Updated dependencies [a45a3f0] + - @ai-billing/polar@0.1.4 diff --git a/examples/chatbot-with-billing-with-clerk/README.md b/examples/chatbot-with-billing-with-clerk/README.md new file mode 100644 index 00000000..2466826b --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/README.md @@ -0,0 +1,73 @@ + + Chatbot with Billing +

Chatbot with AI Billing in Polars

+
+ +

+ This is an open-source template built with Next.js, the AI SDK, and the ai-billing package. It helps you quickly build powerful chatbot applications with built-in monetization and billing capabilities. +

+ +

+ Read Docs · + Features · + Model Providers · + Deploy Your Own · + Running locally +

+
+ +## Features + +- **[ai-billing](https://narev.ai/docs/sdk/ai-billing)** + - Seamless monetization and billing integration for your AI applications + - Powered by [narev.ai](https://narev.ai) +- [Next.js](https://nextjs.org) App Router + - Advanced routing for seamless navigation and performance + - React Server Components (RSCs) and Server Actions for server-side rendering and increased performance +- [AI SDK](https://ai-sdk.dev/docs/introduction) + - Unified API for generating text, structured objects, and tool calls with LLMs + - Hooks for building dynamic chat and generative user interfaces + - Supports OpenAI, Anthropic, Google, xAI, and other model providers via AI Gateway +- [shadcn/ui](https://ui.shadcn.com) + - Styling with [Tailwind CSS](https://tailwindcss.com) + - Component primitives from [Radix UI](https://radix-ui.com) for accessibility and flexibility +- Data Persistence + - [Neon Serverless Postgres](https://vercel.com/marketplace/neon) for saving chat history and user data +- [Auth.js](https://authjs.dev) + - Simple and secure authentication + +## Model Providers + +This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. Models are configured in `lib/ai/models.ts` with per-model provider routing. Included models: Mistral, Moonshot, DeepSeek, OpenAI, and xAI. + +### AI Gateway Authentication + +**For Vercel deployments**: Authentication is handled automatically via OIDC tokens. + +**For non-Vercel deployments**: You need to provide an AI Gateway API key by setting the `AI_GATEWAY_API_KEY` environment variable in your `.env.local` file. + +With the [AI SDK](https://ai-sdk.dev/docs/introduction), you can also switch to direct LLM providers like [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [many more](https://ai-sdk.dev/providers/ai-sdk-providers) with just a few lines of code. + +## Deploy Your Own + +You can deploy your own version of Chatbot with billing to Vercel with one click: + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnarevai%2Fchatbot-with-billing&env=AUTH_SECRET,AI_GATEWAY_API_KEY,POSTGRES_URL,POLAR_ACCESS_TOKEN,POLAR_SERVER&envDefaults=%7B%22POLAR_SERVER%22%3A%22sandbox%22%7D) + +## Running locally + +You will need to use the environment variables [defined in `.env.example`](.env.example) to run Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary. + +> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various AI and authentication provider accounts. + +1. Install Vercel CLI: `npm i -g vercel` +2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link` +3. Download your environment variables: `vercel env pull` + +```bash +pnpm install +pnpm db:migrate # Setup database or apply latest database changes +pnpm dev +``` + +Your app template should now be running on [localhost:3000](http://localhost:3000). diff --git a/examples/chatbot-with-billing-with-clerk/app/(chat)/actions.ts b/examples/chatbot-with-billing-with-clerk/app/(chat)/actions.ts new file mode 100644 index 00000000..455b97c6 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/app/(chat)/actions.ts @@ -0,0 +1,50 @@ +'use server'; + +import { generateText, type UIMessage } from 'ai'; +import { createOpenAI } from '@ai-sdk/openai'; +import { auth } from '@clerk/nextjs/server'; +import type { VisibilityType } from '@/components/chat/visibility-selector'; +import { titlePrompt } from '@/lib/ai/prompts'; +import { + getChatById, + getUserId, + updateChatVisibilityById, +} from '@/lib/db/queries'; +import { getTextFromMessage } from '@/lib/utils'; + +export async function generateTitleFromUserMessage({ + message, +}: { + message: UIMessage; +}) { + const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY }); + const { text } = await generateText({ + model: openai('gpt-4o-mini'), + system: titlePrompt, + prompt: getTextFromMessage(message), + }); + return text + .replace(/^[#*"\s]+/, '') + .replace(/["]+$/, '') + .trim(); +} + +export async function updateChatVisibility({ + chatId, + visibility, +}: { + chatId: string; + visibility: VisibilityType; +}) { + const dbUserId = await getUserId(); + if (!dbUserId) { + throw new Error('Unauthorized'); + } + + const chat = await getChatById({ id: chatId }); + if (!chat || chat.userId !== dbUserId) { + throw new Error('Unauthorized'); + } + + await updateChatVisibilityById({ chatId, visibility }); +} diff --git a/examples/chatbot-with-billing-with-clerk/app/(chat)/api/chat/route.ts b/examples/chatbot-with-billing-with-clerk/app/(chat)/api/chat/route.ts new file mode 100644 index 00000000..f7c9a45e --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/app/(chat)/api/chat/route.ts @@ -0,0 +1,33 @@ +import { auth } from '@clerk/nextjs/server'; +import { deleteChatById, getChatById, getUserId } from '@/lib/db/queries'; +import { ChatbotError } from '@/lib/errors'; + +export async function DELETE(request: Request) { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + if (!id) { + return new ChatbotError('bad_request:api').toResponse(); + } + + const { userId } = await auth(); + + if (!userId) { + return new ChatbotError('unauthorized:chat').toResponse(); + } + + const dbUserId = await getUserId(); + if (!dbUserId) { + return new ChatbotError('unauthorized:chat').toResponse(); + } + + const chat = await getChatById({ id }); + + if (chat?.userId !== dbUserId) { + return new ChatbotError('forbidden:chat').toResponse(); + } + + const deletedChat = await deleteChatById({ id }); + + return Response.json(deletedChat, { status: 200 }); +} diff --git a/examples/chatbot-with-billing-with-clerk/app/(chat)/api/history/route.ts b/examples/chatbot-with-billing-with-clerk/app/(chat)/api/history/route.ts new file mode 100644 index 00000000..275be77c --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/app/(chat)/api/history/route.ts @@ -0,0 +1,58 @@ +import type { NextRequest } from 'next/server'; +import { auth } from '@clerk/nextjs/server'; +import { + deleteAllChatsByUserId, + getChatsByUserId, + getUserId, +} from '@/lib/db/queries'; +import { ChatbotError } from '@/lib/errors'; + +export async function GET(request: NextRequest) { + const { searchParams } = request.nextUrl; + + const limit = Math.min( + Math.max(Number.parseInt(searchParams.get('limit') || '10', 10), 1), + 50, + ); + const startingAfter = searchParams.get('starting_after'); + const endingBefore = searchParams.get('ending_before'); + + if (startingAfter && endingBefore) { + return new ChatbotError( + 'bad_request:api', + 'Only one of starting_after or ending_before can be provided.', + ).toResponse(); + } + + const { userId } = await auth(); + + if (!userId) { + return new ChatbotError('unauthorized:chat').toResponse(); + } + + const dbUserId = await getUserId(); + if (!dbUserId) { + return new ChatbotError('unauthorized:chat').toResponse(); + } + + const chats = await getChatsByUserId({ + id: dbUserId, + limit, + startingAfter, + endingBefore, + }); + + return Response.json(chats); +} + +export async function DELETE() { + const dbUserId = await getUserId(); + + if (!dbUserId) { + return new ChatbotError('unauthorized:chat').toResponse(); + } + + const result = await deleteAllChatsByUserId({ userId: dbUserId }); + + return Response.json(result, { status: 200 }); +} diff --git a/examples/chatbot-with-billing-with-clerk/app/(chat)/chat/[id]/page.tsx b/examples/chatbot-with-billing-with-clerk/app/(chat)/chat/[id]/page.tsx new file mode 100644 index 00000000..2a4680ad --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/app/(chat)/chat/[id]/page.tsx @@ -0,0 +1,27 @@ +import { redirect } from 'next/navigation'; +import type { UIMessage } from 'ai'; +import { auth } from '@clerk/nextjs/server'; +import { ChatShell } from '@/components/chat/shell'; +import { getChatById, getMessagesByChatId, getUserId } from '@/lib/db/queries'; +import { convertToUIMessages } from '@/lib/utils'; + +export default async function ChatPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const { userId } = await auth(); + if (!userId) redirect('/sign-in'); + + const dbUserId = await getUserId(); + if (!dbUserId) redirect('/sign-in'); + + const chat = await getChatById({ id }); + if (chat && chat.userId !== dbUserId) redirect('/'); + + const dbMessages = chat ? await getMessagesByChatId({ id }) : []; + const messages = convertToUIMessages(dbMessages) as UIMessage[]; + + return ; +} diff --git a/examples/chatbot-with-billing-with-clerk/app/(chat)/chat/layout.tsx b/examples/chatbot-with-billing-with-clerk/app/(chat)/chat/layout.tsx new file mode 100644 index 00000000..4d3a86e8 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/app/(chat)/chat/layout.tsx @@ -0,0 +1,7 @@ +export default function ChatLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} diff --git a/examples/chatbot-with-billing-with-clerk/app/(chat)/chat/page.tsx b/examples/chatbot-with-billing-with-clerk/app/(chat)/chat/page.tsx new file mode 100644 index 00000000..2b2ae919 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/app/(chat)/chat/page.tsx @@ -0,0 +1,9 @@ +import { auth } from '@clerk/nextjs/server'; +import { redirect } from 'next/navigation'; +import { generateUUID } from '@/lib/utils'; + +export default async function ChatPage() { + const { userId } = await auth(); + if (!userId) redirect('/sign-in'); + redirect(`/chat/${generateUUID()}`); +} diff --git a/examples/chatbot-with-billing-with-clerk/app/(chat)/db-actions.ts b/examples/chatbot-with-billing-with-clerk/app/(chat)/db-actions.ts new file mode 100644 index 00000000..c949b990 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/app/(chat)/db-actions.ts @@ -0,0 +1,29 @@ +'use server'; + +import { + saveChat as _saveChat, + saveMessages as _saveMessages, + updateChatTitleById as _updateChatTitleById, +} from '@/lib/db/queries'; +import type { DBMessage } from '@/lib/db/schema'; +import type { VisibilityType } from '@/components/chat/visibility-selector'; + +export async function saveChat(args: { + id: string; + userId: string; + title: string; + visibility: VisibilityType; +}) { + return _saveChat(args); +} + +export async function saveMessages(args: { messages: DBMessage[] }) { + return _saveMessages(args); +} + +export async function updateChatTitleById(args: { + chatId: string; + title: string; +}) { + return _updateChatTitleById(args); +} diff --git a/examples/chatbot-with-billing-with-clerk/app/(chat)/layout.tsx b/examples/chatbot-with-billing-with-clerk/app/(chat)/layout.tsx new file mode 100644 index 00000000..d24a1f36 --- /dev/null +++ b/examples/chatbot-with-billing-with-clerk/app/(chat)/layout.tsx @@ -0,0 +1,44 @@ +import '@/lib/ai/chat-setup'; +import { cookies } from 'next/headers'; +import Script from 'next/script'; +import { Suspense } from 'react'; +import { Toaster } from 'sonner'; +import { AppSidebar } from '@/components/chat/app-sidebar'; +import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; +import { auth } from '@clerk/nextjs/server'; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + <> +