You are creating a new plan file in the .agents/plans/ directory.
link from the landing page,
add upgrade under the dropdown
user can go to their polar profile to cancel their plan, etc etc
I need you to use context7 to get the docs for polar and polar combined with better-auth and implement the auth fully in the application.` as the file slug (kebab-case recommended)
2. Gather content - Collect or generate the plan content
3. Create the file - Use the create-plan script to generate the file
The script will automatically:
happy-blue-moon){id}-{slug}.md in .agents/plans/If you have the content ready, run:
For multi-line content, use heredoc:
You can also pipe content:
The created file will have:
The title is automatically formatted from the slug (e.g., my-feature -> My Feature).
user-authentication, api-integration)Create a plan file using the slug from `Look on the landing page, the I want you to implement full payment with all the parameters on the pricing page implemented, in all aspects.
link from the landing page,
add upgrade under the dropdown
user can go to their polar profile to cancel their plan, etc etc
I need you to use context7 to get the docs for polar and polar combined with better-auth and implement the auth fully in the application.` and appropriate content for the planning task.
Planning slug and docs retrieval
Checking plans directory
Planning directory verification steps
Preparing parallel library resolution
Checking current integration requirements first by pulling official docs for Polar and Better Auth via Context7, then I’ll draft the plan content and generate the .agents/plans file.
Planning parallel doc queries
Planning targeted documentation queries
Planning docs for auth client
Considering docs resolution strategy
Planning repo context gathering
Found 18 match(es) in 14 file(s)
/Users/duncan/dev/bmrks/components/landing.tsx
15: import { LandingPricing } from "@/components/landing-pricing";
/Users/duncan/dev/bmrks/lib/auth-client.ts
1: import { createAuthClient } from "better-auth/react";
/Users/duncan/dev/bmrks/components/ui/context-menu.tsx
15: <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
/Users/duncan/dev/bmrks/lib/auth.ts
1: import { betterAuth } from "better-auth";
2: import { createAuthMiddleware } from "better-auth/api";
3: import { prismaAdapter } from "better-auth/adapters/prisma";
4: import { nextCookies } from "better-auth/next-js";
/Users/duncan/dev/bmrks/components/ui/drawer.tsx
23: return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
51: <DrawerPortal data-slot="drawer-portal">
/Users/duncan/dev/bmrks/package.json
35: "better-auth": "^1.4.1",
/Users/duncan/dev/bmrks/components/ui/dropdown-menu.tsx
14: return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
/Users/duncan/dev/bmrks/components/ui/sheet.tsx
23: return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
/Users/duncan/dev/bmrks/components/ui/alert-dialog.tsx
21: <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
/Users/duncan/dev/bmrks/components/ui/hover-card.tsx
30: <PreviewCardPrimitive.Portal data-slot="hover-card-portal">
/Users/duncan/dev/bmrks/app/api/auth/[...all]/route.ts
2: import { toNextJsHandler } from "better-auth/next-js";
/Users/duncan/dev/bmrks/components/ui/chart.tsx
58: "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
/Users/duncan/dev/bmrks/components/ui/dialog.tsx
19: return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
/Users/duncan/dev/bmrks/components/ui/menubar.tsx
48: return <DropdownMenuPortal data-slot="menubar-portal" {...props} />
[Agent Usage Reminder]
You called a search/fetch tool directly without leveraging specialized agents.
RECOMMENDED: Use task with explore/librarian agents for better results:
```
// Parallel exploration - fire multiple agents simultaneously
task(agent="explore", prompt="Find all files matching pattern X")
task(agent="explore", prompt="Search for implementation of Y")
task(agent="librarian", prompt="Lookup documentation for Z")
// Then continue your work while they run in background
// System will notify you when each completes
```
WHY:
- Agents can perform deeper, more thorough searches
- Background tasks run in parallel, saving time
- Specialized agents have domain expertise
- Reduces context window usage in main session
ALWAYS prefer: Multiple parallel task calls > Direct tool calls
Planning UI inspection
I pulled the relevant Polar + Better Auth docs and now I’m mapping that into this repo’s existing landing/pricing/auth structure so the plan file is implementation-ready.
Found 133 match(es) in 20 file(s)
/Users/duncan/dev/bmrks/components/landing-pricing.tsx
137: const [billingCycle, setBillingCycle] = useState<BillingCycle>("yearly");
138: const selectedPricing = PRO_PLAN_PRICING[billingCycle];
149: Choose the plan that fits your workflow.
152: <BillingToggle value={billingCycle} onChange={setBillingCycle} />
/Users/duncan/dev/bmrks/components/settings-dialog.tsx
20: import type { ProfileData } from "@/components/dashboard-content";
/Users/duncan/dev/bmrks/components/bulk-move-dialog.tsx
18: } from "@/components/ui/dropdown-menu";
/Users/duncan/dev/bmrks/components/signup-form.tsx
80: router.push("/dashboard");
/Users/duncan/dev/bmrks/components/multi-select-toolbar.tsx
10: } from "@/components/ui/dropdown-menu";
/Users/duncan/dev/bmrks/components/login-form.tsx
70: router.push("/dashboard");
/Users/duncan/dev/bmrks/components/landing.tsx
14: import { DashboardDemoLazy as DashboardDemo } from "@/components/dashboard-demo-lazy";
15: import { LandingPricing } from "@/components/landing-pricing";
/Users/duncan/dev/bmrks/components/header.tsx
12: } from "@/components/ui/dropdown-menu";
54: import type { ProfileData } from "@/components/dashboard-content";
/Users/duncan/dev/bmrks/components/oauth-button.tsx
32: callbackURL: "/dashboard",
/Users/duncan/dev/bmrks/components/bookmark-list.tsx
12: } from "@/components/ui/context-menu";
29: import { ContextMenuSeparator } from "@/components/ui/context-menu";
/Users/duncan/dev/bmrks/components/dashboard-demo-lazy.tsx
6: () => import("@/components/dashboard-demo").then((m) => m.DashboardDemo),
/Users/duncan/dev/bmrks/components/export-dialog.tsx
18: } from "@/components/ui/dropdown-menu";
/Users/duncan/dev/bmrks/components/dashboard-content.tsx
12: import { BookmarkListSkeleton } from "@/components/dashboard-skeleton";
/Users/duncan/dev/bmrks/components/ui/context-menu.tsx
4: import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
10: return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
15: <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
25: data-slot="context-menu-trigger"
54: data-slot="context-menu-content"
65: <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
78: data-slot="context-menu-label"
97: data-slot="context-menu-item"
101: "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
109: function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
111: <ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
120: }: ContextMenuPrimitive.SubmenuTrigger.Props & {
124: <ContextMenuPrimitive.SubmenuTrigger
125: data-slot="context-menu-sub-trigger"
135: </ContextMenuPrimitive.SubmenuTrigger>
144: data-slot="context-menu-sub-content"
160: data-slot="context-menu-checkbox-item"
184: data-slot="context-menu-radio-group"
197: data-slot="context-menu-radio-item"
221: data-slot="context-menu-separator"
234: data-slot="context-menu-shortcut"
235: className={cn("text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
/Users/duncan/dev/bmrks/components/ui/navigation-menu.tsx
1: import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
14: data-slot="navigation-menu"
16: "max-w-max group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
33: data-slot="navigation-menu-list"
49: data-slot="navigation-menu-item"
57: "bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all focus-visible:ring-[3px] focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center disabled:pointer-events-none outline-none"
67: data-slot="navigation-menu-trigger"
72: <IconChevronDown className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180" aria-hidden="true" />
83: data-slot="navigation-menu-content"
85: [Omitted long matching line]
128: data-slot="navigation-menu-link"
129: className={cn("data-active:focus:bg-muted data-active:hover:bg-muted data-active:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4 [[data-slot=navigation-menu-content]_&]:rounded-md", className)}
141: data-slot="navigation-menu-indicator"
/Users/duncan/dev/bmrks/components/ui/calendar.tsx
68: dropdowns: cn(
70: defaultClassNames.dropdowns
72: dropdown_root: cn(
73: "relative cn-calendar-dropdown-root rounded-(--cell-radius)",
74: defaultClassNames.dropdown_root
76: dropdown: cn(
78: defaultClassNames.dropdown
/Users/duncan/dev/bmrks/components/ui/menubar.tsx
4: import { Menu as MenuPrimitive } from "@base-ui/react/menu"
5: import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar"
22: } from "@/components/ui/dropdown-menu"
28: data-slot="menubar"
36: return <DropdownMenu data-slot="menubar-menu" {...props} />
42: return <DropdownMenuGroup data-slot="menubar-group" {...props} />
48: return <DropdownMenuPortal data-slot="menubar-portal" {...props} />
57: data-slot="menubar-trigger"
76: data-slot="menubar-content"
94: data-slot="menubar-item"
97: className={cn("focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg:not([class*='size-'])]:size-4 group/menubar-item", className)}
111: data-slot="menubar-checkbox-item"
133: return <DropdownMenuRadioGroup data-slot="menubar-radio-group" {...props} />
143: data-slot="menubar-radio-item"
168: data-slot="menubar-label"
182: data-slot="menubar-separator"
195: data-slot="menubar-shortcut"
196: className={cn("text-muted-foreground group-focus/menubar-item:text-accent-foreground text-xs tracking-widest ml-auto", className)}
205: return <DropdownMenuSub data-slot="menubar-sub" {...props} />
217: data-slot="menubar-sub-trigger"
231: data-slot="menubar-sub-content"
/Users/duncan/dev/bmrks/components/ui/dropdown-menu.tsx
4: import { Menu as MenuPrimitive } from "@base-ui/react/menu"
10: return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
14: return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
18: return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
43: data-slot="dropdown-menu-content"
53: return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
65: data-slot="dropdown-menu-label"
84: data-slot="dropdown-menu-item"
88: "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
96: function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
97: return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
105: }: MenuPrimitive.SubmenuTrigger.Props & {
109: <MenuPrimitive.SubmenuTrigger
110: data-slot="dropdown-menu-sub-trigger"
120: </MenuPrimitive.SubmenuTrigger>
134: data-slot="dropdown-menu-sub-content"
153: data-slot="dropdown-menu-checkbox-item"
163: data-slot="dropdown-menu-checkbox-item-indicator"
178: data-slot="dropdown-menu-radio-group"
191: data-slot="dropdown-menu-radio-item"
200: data-slot="dropdown-menu-radio-item-indicator"
218: data-slot="dropdown-menu-separator"
231: data-slot="dropdown-menu-shortcut"
232: className={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
/Users/duncan/dev/bmrks/components/ui/item.tsx
49: xs: "gap-2 px-2.5 py-2 [[data-slot=dropdown-menu-content]_&]:p-0",
/Users/duncan/dev/bmrks/components/ui/sidebar.tsx
462: data-slot="sidebar-menu"
463: data-sidebar="menu"
473: data-slot="sidebar-menu-item"
474: data-sidebar="menu-item"
475: className={cn("group/menu-item relative", className)}
482: "ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0",
526: slot: "sidebar-menu-button",
527: sidebar: "menu-button",
570: "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-md p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0",
572: "peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-open:opacity-100 md:opacity-0",
580: slot: "sidebar-menu-action",
581: sidebar: "menu-action",
592: data-slot="sidebar-menu-badge"
593: data-sidebar="menu-badge"
595: "text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 rounded-md px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 flex items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden",
617: data-slot="sidebar-menu-skeleton"
618: data-sidebar="menu-skeleton"
625: data-sidebar="menu-skeleton-icon"
630: data-sidebar="menu-skeleton-text"
644: data-slot="sidebar-menu-sub"
645: data-sidebar="menu-sub"
658: data-slot="sidebar-menu-sub-item"
659: data-sidebar="menu-sub-item"
660: className={cn("group/menu-sub-item relative", className)}
690: slot: "sidebar-menu-sub-button",
691: sidebar: "menu-sub-button",
[Agent Usage Reminder]
You called a search/fetch tool directly without leveraging specialized agents.
RECOMMENDED: Use task with explore/librarian agents for better results:
```
// Parallel exploration - fire multiple agents simultaneously
task(agent="explore", prompt="Find all files matching pattern X")
task(agent="explore", prompt="Search for implementation of Y")
task(agent="librarian", prompt="Lookup documentation for Z")
// Then continue your work while they run in background
// System will notify you when each completes
```
WHY:
- Agents can perform deeper, more thorough searches
- Background tasks run in parallel, saving time
- Specialized agents have domain expertise
- Reduces context window usage in main session
ALWAYS prefer: Multiple parallel task calls > Direct tool calls
Identifying key files to read
Inspecting landing component for anchors
Exploring subscription data models
Found 5 match(es) in 2 file(s)
/Users/duncan/dev/bmrks/components/landing-pricing.tsx
137: const [billingCycle, setBillingCycle] = useState<BillingCycle>("yearly");
138: const selectedPricing = PRO_PLAN_PRICING[billingCycle];
149: Choose the plan that fits your workflow.
152: <BillingToggle value={billingCycle} onChange={setBillingCycle} />
/Users/duncan/dev/bmrks/components/ui/chart.tsx
58: "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
[Agent Usage Reminder]
You called a search/fetch tool directly without leveraging specialized agents.
RECOMMENDED: Use task with explore/librarian agents for better results:
```
// Parallel exploration - fire multiple agents simultaneously
task(agent="explore", prompt="Find all files matching pattern X")
task(agent="explore", prompt="Search for implementation of Y")
task(agent="librarian", prompt="Lookup documentation for Z")
// Then continue your work while they run in background
// System will notify you when each completes
```
WHY:
- Agents can perform deeper, more thorough searches
- Background tasks run in parallel, saving time
- Specialized agents have domain expertise
- Reduces context window usage in main session
ALWAYS prefer: Multiple parallel task calls > Direct tool calls
Checking billing dependencies
Drafting concise action plan
Considering plugin client setup
Seeking auth client setup info
Checking for polar client plugin
Drafting concise plan content
Planning slugged plan file creation
lsDrafting implementation plan outline
npx tsx ~/.config/opencode/scripts/create-plan.ts "landing-page-full-payment-pricing-upgrade-polar-profile-cancel-better-auth" <<'HEREDOC'
## Outcome
Ship end-to-end paid plan flow: landing/pricing checkout, in-app upgrade entry, Polar customer portal cancel/manage, and Better Auth + Polar subscription sync.
## Context7 constraints (authoritative)
- Better Auth Polar plugin: `polar({ client, createCustomerOnSignUp, use: [checkout(), portal(), webhooks()] })`.
- Client actions: `authClient.checkout(...)`, `authClient.customer.portal()`, `authClient.customer.subscriptions.list(...)`.
- Polar checkout core params to wire end-to-end: `product_price_id` (or mapped slug), `success_url`, `customer_email`, `metadata`, optional discount/seat params when supported.
## Plan
1. Baseline + config
- Audit current files: `components/landing-pricing.tsx`, `components/landing.tsx`, `components/header.tsx`, `lib/auth.ts`, `lib/auth-client.ts`, `prisma/schema.prisma`.
- Add deps: `@polar-sh/better-auth`, `@polar-sh/sdk`.
- Add env contract: `POLAR_ACCESS_TOKEN`, `POLAR_WEBHOOK_SECRET`, `POLAR_SERVER`, `POLAR_PRO_MONTHLY_PRODUCT_ID` (or price ID), `POLAR_PRO_YEARLY_PRODUCT_ID`, `NEXT_PUBLIC_APP_URL`.
2. Server auth + billing integration
- Extend `lib/auth.ts` with Polar client and Better Auth Polar plugin.
- Enable: checkout (monthly/yearly mappings + success/return URLs + auth-only policy), portal, webhooks.
- Add webhook handlers to persist entitlement state in app DB (active/canceled/trialing, plan, renewal/cancel timestamps, Polar customer id).
- Regenerate/update Better Auth Prisma models; apply migration/push safely.
3. Client auth integration
- Extend `lib/auth-client.ts` with `polarClient()` so checkout/portal/subscription methods are typed on client.
- Add a small billing helper wrapper for checkout payload creation and error handling.
4. Landing + pricing conversion
- Replace static `/signup` CTA in `components/landing-pricing.tsx` with plan-aware actions:
- signed out: redirect to signup/login with intended plan + cycle.
- signed in: call `authClient.checkout` with mapped monthly/yearly product + metadata (`source: landing_pricing`, cycle, user id/ref id if needed).
- Ensure all pricing parameters used in app-level payload builder; keep yearly/monthly toggle behavior intact.
- Add pricing-to-checkout navigation from landing where needed.
5. Dashboard dropdown upgrade + portal
- In `components/header.tsx` add:
- `Upgrade` item (opens Pro checkout flow).
- `Manage Billing` / `Billing Portal` item (calls `authClient.customer.portal()` for cancel/manage).
- Hide/disable Upgrade when user already has active Pro; show manage billing instead.
6. Entitlement sync + feature gating
- Derive `isPro` from webhook-synced state (not optimistic client state).
- Update relevant server/client checks for Pro-only features.
- Add fallback reconciliation path using `authClient.customer.subscriptions.list({ active: true })` for drift recovery.
7. Verification
- Sandbox flow test: new checkout, upgrade, renewal event, cancellation in Polar portal, access downgrade.
- Verify webhook signature handling and idempotency.
- Run `bun run typecheck`, `bun run lint`, focused tests, and `bun run build`.
## Done criteria
- Pricing cards initiate correct Polar checkout for monthly/yearly.
- Dashboard dropdown contains working Upgrade + Billing Portal actions.
- User can cancel/manage in Polar portal; app entitlement updates via webhook.
- Auth + billing integration fully typed, migrated, and passing lint/typecheck/build.
## Unresolved questions
- Exact Polar product/price IDs and whether checkout should use product IDs vs price IDs in this app.
- Final Pro feature gate list (which capabilities require active subscription now).
- Preferred post-checkout and post-portal return URLs (dashboard only vs contextual return).
HEREDOCPlanning file content verification
Verifying frontmatter presence
Summarizing plan creation details
Created and verified.
npx tsx ~/.config/opencode/scripts/create-plan.ts with slug landing-page-full-payment-pricing-upgrade-polar-profile-cancel-better-auth..agents/plans/gentle-jade-leaf-landing-page-full-payment-pricing-upgrade-polar-profile-cancel-better-auth.md.## Unresolved questions section at the end of the plan per plan-mode guidance.[search-mode]
MAXIMIZE SEARCH EFFORT. Launch multiple background agents IN PARALLEL:
not soft, read the @components/landing-pricing.tsx and implement all of them, not some, all of them.
Planning multi-agent search and todos
Quick summary of that task-tool run: it returned a task_id (ses_38d096f9effe72COm8I2Qwl3ZZ) but no actual result payload, so no interview findings were produced yet.
I’m now continuing with full implementation (not partial): wiring Polar + Better Auth, upgrading pricing actions, and adding upgrade/manage-billing controls in the dropdown. I’ll verify everything with typecheck/lint/build afterward.
System Reminder
[BACKGROUND TASK COMPLETED] **ID:** `bg_f9b40869` **Description:** Map auth and session data flow for billing integration **Duration:** 23s **1 task still in progress.** You WILL be notified when ALL complete. Do NOT poll - continue productive work. Use `background_output(task_id="bg_f9b40869")` to retrieve this result when ready.
System Reminder
[ALL BACKGROUND TASKS COMPLETE] **Completed:** - `bg_f9b40869`: Map auth and session data flow for billing integration - `bg_369bd9be`: Map landing pricing and checkout trigger integration points Use `background_output(task_id="<id>")` to retrieve each result.
Planning exhaustive code exploration
Great — I’ve pulled the completed background findings and they align with your instruction: implement all pricing behavior, not partial.
Now I’m moving straight into code changes across pricing, auth, billing state, and dropdown UX so monthly/yearly checkout, upgrade, and portal cancellation all work end-to-end. I’ll run full verification after implementation.
[SYSTEM DIRECTIVE: OH-MY-OPENCODE - TODO CONTINUATION]
Incomplete tasks remain in your todo list. Continue working on the next pending task.
[Status: 0/6 completed, 6 remaining]
Remaining tasks:
Planning billing integration
Reconfirming documentation completeness
Marking progress and planning deep scan
I’ve refreshed docs with Context7 and confirmed the concrete APIs we need (authClient.checkout, authClient.customer.portal, subscription list, Polar webhook plugin). Next I’m mapping every integration point in this repo before patching.
Found 255 match(es) in 39 file(s)
/Users/duncan/dev/bmrks/instrumentation-client.ts
9: person_profiles: "identified_only",
/Users/duncan/dev/bmrks/server/index.ts
1: export { router, type Router } from "./router";
2: export { base, authed } from "./context";
/Users/duncan/dev/bmrks/lib/feed.ts
45: const profileUrl = `${baseUrl}/u/${user.username}`;
48: ? `${profileUrl}/feed.xml?group=${encodeURIComponent(slug)}`
49: : `${profileUrl}/feed.xml`;
60: const guid = bookmark.url || `${profileUrl}#${bookmark.title}`;
78: <link>${escapeXml(profileUrl)}</link>
94: const profileUrl = `${baseUrl}/u/${user.username}`;
97: ? `${profileUrl}/feed.atom?group=${encodeURIComponent(slug)}`
98: : `${profileUrl}/feed.atom`;
108: const id = bookmark.url || `${profileUrl}#${encodeURIComponent(bookmark.title)}`;
128: <link href="${escapeXml(profileUrl)}" />
134: <id>${escapeXml(profileUrl)}</id>
/Users/duncan/dev/bmrks/hooks/use-focus-refetch.ts
5: import { orpc } from "@/lib/orpc";
21: queryClient.invalidateQueries({ queryKey: orpc.group.key() });
24: queryKey: orpc.bookmark.list.key({ input: { groupId: group.id } }),
/Users/duncan/dev/bmrks/lib/auth-client.ts
3: export const authClient = createAuthClient();
9: useSession,
12: } = authClient;
/Users/duncan/dev/bmrks/components/landing-pricing.tsx
39: label: "Public profile",
/Users/duncan/dev/bmrks/server/context.ts
1: import { ORPCError, os } from "@orpc/server";
7: const session: Session | null = await auth.api.getSession({
19: export const authed = base.use(({ context, next }) => {
/Users/duncan/dev/bmrks/app/rpc/[[...rest]]/route.ts
1: import { RPCHandler } from "@orpc/server/fetch";
2: import { onError } from "@orpc/server";
3: import { router } from "@/server";
5: const handler = new RPCHandler(router, {
/Users/duncan/dev/bmrks/app/api/og/route.tsx
5: import { getPublicProfileData } from "@/server/queries/public-profile";
/Users/duncan/dev/bmrks/app/login/page.tsx
3: import { getSession } from "@/lib/auth-server";
7: const session = await getSession();
/Users/duncan/dev/bmrks/server/router.ts
17: } from "./procedures/bookmarks";
22: } from "./procedures/profile";
23: import { getPublicProfile } from "./procedures/public";
25: export const router = base.router({
44: profile: {
54: export type Router = typeof router;
/Users/duncan/dev/bmrks/lib/orpc.ts
3: import type { RouterClient } from "@orpc/server";
4: import { RPCLink } from "@orpc/client/fetch";
5: import { createORPCClient } from "@orpc/client";
6: import { createTanstackQueryUtils } from "@orpc/tanstack-query";
20: export const orpc = createTanstackQueryUtils(client);
/Users/duncan/dev/bmrks/components/dashboard-demo.tsx
298: const router = useRouter();
333: router.push("/login");
/Users/duncan/dev/bmrks/app/u/[username]/page.tsx
4: import { getPublicProfileData } from "@/server/queries/public-profile";
5: import { getSession } from "@/lib/auth-server";
7: import { PublicProfileContent } from "./public-profile-content";
64: type: "profile",
84: getSession(),
/Users/duncan/dev/bmrks/server/procedures/public.ts
3: import { ORPCError } from "@orpc/server";
4: import { getPublicProfileData } from "../queries/public-profile";
/Users/duncan/dev/bmrks/components/toaster-provider.tsx
6: () => import("@/components/ui/sonner").then((m) => m.Toaster),
/Users/duncan/dev/bmrks/server/procedures/profile.ts
1: import { authed } from "../context";
5: import { ORPCError } from "@orpc/server";
7: export const getProfile = authed.handler(async ({ context }) => {
23: export const updateProfile = authed
65: export const checkUsername = authed
/Users/duncan/dev/bmrks/components/settings-dialog.tsx
10: import { toast } from "sonner";
11: import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
13: import { authClient } from "@/lib/auth-client";
14: import { client, orpc } from "@/lib/orpc";
67: profile?: ProfileData;
75: profile,
78: const router = useRouter();
100: toast.error("Only PNG, JPEG, WebP, and GIF files are supported");
105: toast.error("Avatar must be 2MB or smaller");
125: toast.success("Avatar updated");
126: router.refresh();
128: toast.error(error instanceof Error ? error.message : "Failed to upload avatar");
150: toast.success("Avatar removed");
151: router.refresh();
153: toast.error(error instanceof Error ? error.message : "Failed to remove avatar");
166: const { error } = await authClient.updateUser({ name: name.trim() });
170: toast.error("Failed to update name");
175: toast.success("Name updated");
177: router.refresh();
182: toast.error("Install the Chrome extension to import bookmarks", {
192: const loadingId = toast.loading("Importing browser bookmarks...");
202: toast.error("Please log in to import bookmarks");
204: toast.error(result.message || "Failed to import bookmarks. Please try again.");
214: toast.success(parts.join(". "));
216: queryClient.invalidateQueries({ queryKey: orpc.bookmark.key() });
217: queryClient.invalidateQueries({ queryKey: orpc.group.key() });
219: toast.error("Failed to import bookmarks. Please try again.");
221: toast.dismiss(loadingId);
236: <TabsTrigger value="profile">Public Profile</TabsTrigger>
363: <TabsContent value="profile">
364: {profile && <ProfileTab profile={profile} onOpenChange={onOpenChange} />}
373: profile: ProfileData;
377: function ProfileTab({ profile, onOpenChange }: ProfileTabProps) {
378: const router = useRouter();
380: const [username, setUsername] = useState(profile.username ?? "");
381: const [bio, setBio] = useState(profile.bio ?? "");
382: const [github, setGithub] = useState(profile.github ?? "");
383: const [twitter, setTwitter] = useState(profile.twitter ?? "");
385: profile.website?.replace(/^https?:\/\//, "") ?? "",
388: profile.isProfilePublic,
408: ...orpc.profile.checkUsername.queryOptions({
422: const updateMutation = useMutation({
423: mutationFn: (data: Parameters<typeof client.profile.update>[0]) =>
424: client.profile.update(data),
427: toast.success("Profile updated");
429: router.refresh();
432: toast.error(err.message || "Failed to update profile");
439: const originalWebsite = profile.website?.replace(/^https?:\/\//, "") ?? "";
441: username !== (profile.username ?? "") ||
442: bio !== (profile.bio ?? "") ||
443: github !== (profile.github ?? "") ||
444: twitter !== (profile.twitter ?? "") ||
446: isProfilePublic !== profile.isProfilePublic;
483: toast.success("Link copied");
/Users/duncan/dev/bmrks/app/u/[username]/feed-response.ts
2: import { getPublicProfileData } from "@/server/queries/public-profile";
/Users/duncan/dev/bmrks/components/dashboard-content.tsx
5: import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
6: import { toast } from "sonner";
39: import { client, orpc } from "@/lib/orpc";
60: profile: ProfileData;
63: const groupListKey = () => orpc.group.list.queryKey() as readonly unknown[];
65: orpc.bookmark.list.queryKey({
73: profile,
92: ...orpc.group.list.queryOptions(),
99: const hasUsername = profile.username !== null;
142: ...orpc.bookmark.list.queryOptions({
157: ...orpc.bookmark.list.queryOptions({
168: const createBookmarkMutation = useMutation({
169: ...orpc.bookmark.create.mutationOptions(),
172: queryKey: orpc.bookmark.list.queryKey({
176: await queryClient.cancelQueries({ queryKey: orpc.group.list.queryKey() });
227: toast.error("Failed to create bookmark");
234: queryKey: orpc.bookmark.list.key({
238: queryClient.invalidateQueries({ queryKey: orpc.group.key() });
242: const updateBookmarkMutation = useMutation({
267: await queryClient.cancelQueries({ queryKey: orpc.group.list.queryKey() });
269: queryKey: orpc.bookmark.list.queryKey({
275: queryKey: orpc.bookmark.list.queryKey({
385: toast.error("Failed to update bookmark");
390: queryKey: orpc.bookmark.list.key({
397: queryKey: orpc.bookmark.list.key({
402: queryClient.invalidateQueries({ queryKey: orpc.group.key() });
406: const createGroupMutation = useMutation({
416: await queryClient.cancelQueries({ queryKey: orpc.group.list.queryKey() });
457: toast.error("Failed to create group");
460: queryClient.invalidateQueries({ queryKey: orpc.group.key() });
464: const deleteGroupMutation = useMutation({
465: ...orpc.group.delete.mutationOptions(),
467: await queryClient.cancelQueries({ queryKey: orpc.group.list.queryKey() });
500: toast.error("Failed to delete group");
503: queryClient.invalidateQueries({ queryKey: orpc.group.key() });
507: const refetchBookmarkMutation = useMutation({
512: queryKey: orpc.bookmark.list.key({
516: toast.success("Metadata refreshed");
519: toast.error("Failed to refresh metadata");
523: const bulkDeleteMutation = useMutation({
536: queryKey: orpc.bookmark.list.queryKey({
540: await queryClient.cancelQueries({ queryKey: orpc.group.list.queryKey() });
585: toast.error("Failed to delete bookmarks");
590: queryKey: orpc.bookmark.list.key({
595: queryClient.invalidateQueries({ queryKey: orpc.group.key() });
599: const bulkMoveMutation = useMutation({
613: queryKey: orpc.bookmark.list.queryKey({
618: queryKey: orpc.bookmark.list.queryKey({
622: await queryClient.cancelQueries({ queryKey: orpc.group.list.queryKey() });
697: toast.error("Failed to move bookmarks");
702: queryKey: orpc.bookmark.list.key({
709: queryKey: orpc.bookmark.list.key({
714: queryClient.invalidateQueries({ queryKey: orpc.group.key() });
718: const setVisibilityMutation = useMutation({
719: ...orpc.bookmark.setVisibility.mutationOptions(),
723: queryKey: orpc.bookmark.list.queryKey({
749: toast.error("Failed to update visibility");
754: queryKey: orpc.bookmark.list.key({
762: const bulkSetVisibilityMutation = useMutation({
763: ...orpc.bookmark.bulkSetVisibility.mutationOptions(),
767: queryKey: orpc.bookmark.list.queryKey({
793: toast.error("Failed to update visibility");
798: queryKey: orpc.bookmark.list.key({
806: const setGroupVisibilityMutation = useMutation({
807: ...orpc.group.setVisibility.mutationOptions(),
809: await queryClient.cancelQueries({ queryKey: orpc.group.list.queryKey() });
823: toast.success(
834: toast.error("Failed to update group visibility");
837: queryClient.invalidateQueries({ queryKey: orpc.group.key() });
908: toast.success(`Copied ${selectedIds.size} URLs`);
910: toast.error("No URLs to copy");
922: toast.success(`Moved ${selectedIds.size} bookmarks`);
933: toast.success(`Deleted ${selectedIds.size} bookmarks`);
1101: toast.success("Bookmark hidden from public profile");
1104: toast.success("Bookmark visible on public profile");
1116: toast.success(`${selectedIds.size} bookmarks made public`);
1128: toast.success(`${selectedIds.size} bookmarks made private`);
1263: userImage={profile.image}
1264: username={profile.username}
1265: profile={profile}
/Users/duncan/dev/bmrks/components/signup-form.tsx
34: const router = useRouter();
80: router.push("/dashboard");
/Users/duncan/dev/bmrks/components/header.tsx
75: profile?: ProfileData;
94: profile,
100: const router = useRouter();
121: router.push("/login");
442: ? "All bookmarks in this group will become publicly visible on your profile."
443: : "All bookmarks in this group will no longer be publicly visible on your profile."}
497: profile={profile}
/Users/duncan/dev/bmrks/server/procedures/bookmarks.ts
1: import { authed } from "../context";
16: import { ORPCError } from "@orpc/server";
28: export const listBookmarks = authed
41: export const createBookmark = authed
96: export const updateBookmark = authed
120: export const deleteBookmark = authed
129: export const listGroups = authed.handler(async ({ context }) => {
148: export const createGroup = authed
160: export const updateGroup = authed
171: export const deleteGroup = authed
180: export const refetchBookmark = authed
204: export const bulkDeleteBookmarks = authed
213: export const bulkMoveBookmarks = authed
228: export const setBookmarkVisibility = authed
238: export const bulkSetVisibility = authed
253: export const setGroupVisibility = authed
/Users/duncan/dev/bmrks/components/oauth-button.tsx
5: import { toast } from "sonner";
36: toast.error(error.message ?? "Something went wrong. Please try again.");
/Users/duncan/dev/bmrks/components/export-dialog.tsx
25: import { toast } from "sonner";
104: toast.success(
279: toast.success(
/Users/duncan/dev/bmrks/app/changelog/page.tsx
50: { text: "RSS/Atom feeds for public profiles", pr: 49 },
61: { text: "Twitter profile pictures showing generic X favicon instead of actual avatars", pr: 50 },
65: { text: "Broken favicons, missing referrer, and public profile auto-enable" },
77: { text: "Settings dialog with profile management and name editing", pr: 35 },
/Users/duncan/dev/bmrks/components/login-form.tsx
31: const router = useRouter();
70: router.push("/dashboard");
/Users/duncan/dev/bmrks/app/api/avatar/route.ts
6: import { getSession } from "@/lib/auth-server";
129: const session = await getSession();
227: const session = await getSession();
/Users/duncan/dev/bmrks/app/privacy/page.tsx
68: we collect your name, email address, and profile photo (if
/Users/duncan/dev/bmrks/app/signup/page.tsx
3: import { getSession } from "@/lib/auth-server";
7: const session = await getSession();
/Users/duncan/dev/bmrks/app/layout.tsx
6: import { ToasterProvider } from "@/components/toaster-provider";
/Users/duncan/dev/bmrks/app/api/extension/import/route.ts
2: import { getSession } from "@/lib/auth-server";
43: const session = await getSession();
/Users/duncan/dev/bmrks/app/dashboard/page.tsx
3: import { getSession } from "@/lib/auth-server";
9: const session = await getSession();
75: profile={
/Users/duncan/dev/bmrks/lib/orpc.server.ts
3: import { createRouterClient } from "@orpc/server";
4: import { router } from "@/server";
6: export const serverClient = createRouterClient(router);
/Users/duncan/dev/bmrks/components/ui/sonner.tsx
4: import { Toaster as Sonner, type ToasterProps } from "sonner"
13: className="toaster group"
39: toastOptions={{
41: toast: "cn-toast",
/Users/duncan/dev/bmrks/app/api/extension/bookmark/check/route.ts
2: import { getSession } from "@/lib/auth-server";
52: const session = await getSession();
/Users/duncan/dev/bmrks/app/api/extension/bookmark/route.ts
2: import { getSession } from "@/lib/auth-server";
144: const session = await getSession();
/Users/duncan/dev/bmrks/lib/auth-server.ts
6: export async function getSession(): Promise<Session | null> {
8: return auth.api.getSession({ headers: headersList });
/Users/duncan/dev/bmrks/app/admin/page.tsx
5: import { getSession } from "@/lib/auth-server";
167: const session = await getSession();
No matches found
Preparing key file reads
Inspecting auth route conflicts
Evaluating better-auth dependency usage
Designing subscription data model and checkout integration
Reassessing OAuth redirect handling
Planning backend integration
Outlining Polar integration plan
bun add @polar-sh/better-auth @polar-sh/sdkExploring type definitions with grep