Skip to content

Implement Free Trial#2198

Merged
yujonglee merged 13 commits intomainfrom
trial
Dec 10, 2025
Merged

Implement Free Trial#2198
yujonglee merged 13 commits intomainfrom
trial

Conversation

@yujonglee
Copy link
Contributor

No description provided.

@netlify
Copy link

netlify bot commented Dec 10, 2025

Deploy Preview for hyprnote-storybook ready!

Name Link
🔨 Latest commit 37298be
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/6939086fda861c000876aa58
😎 Deploy Preview https://deploy-preview-2198--hyprnote-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Dec 10, 2025

Deploy Preview for hyprnote ready!

Name Link
🔨 Latest commit 37298be
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/6939086fa28eb000087bd7b4
😎 Deploy Preview https://deploy-preview-2198--hyprnote.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This PR introduces OpenAPI documentation generation with a typed API client, implements a trial billing feature with Stripe integration and RPC checks, refactors desktop onboarding to use a state machine instead of inline navigation, updates authentication middleware to handle case-insensitive bearer tokens, and replaces model-download onboarding with a configuration notice.

Changes

Cohort / File(s) Summary
OpenAPI & API Documentation
apps/api/src/openapi.ts, apps/api/src/scripts/generate-openapi.ts, apps/api/openapi.gen.json, apps/api/src/index.ts
Introduces centralized OpenAPI 3.1.0 documentation module, adds generation script that outputs openapi.gen.json and generates a typed API client via @hey-api/openapi-ts, and updates API index to reference the generated spec.
Billing & Trial Routes
apps/api/src/routes/billing.ts, apps/api/src/routes/rpc.ts, supabase/migrations/20250101000004_can_start_trial.sql
Adds /start-trial POST endpoint with Stripe customer/subscription creation and trial eligibility checks, /can-start-trial GET RPC endpoint, and new can_start_trial() PostgreSQL function with security controls.
API Middleware & Bindings
apps/api/src/middleware/supabase.ts, apps/api/src/middleware/load-test-auth.ts, apps/api/src/hono-bindings.ts
Upgrades bearer token extraction to case-insensitive regex parsing, stores Supabase client in request context for downstream middleware, and extends Hono bindings type to include optional supabaseClient field.
API Configuration & Routing
apps/api/src/env.ts, apps/api/src/routes/constants.ts, apps/api/src/routes/index.ts, apps/api/src/routes/health.ts, apps/api/src/routes/llm.ts, apps/api/src/routes/stt.ts, apps/api/src/routes/webhook.ts
Adds Stripe price ID environment variables, replaces API tag enums (INTERNAL/APP/WEBHOOK → PRIVATE_SKIP_OPENAPI/PRIVATE), registers new billing and RPC routes, simplifies route OpenAPI metadata and response schemas.
API Package & Dependencies
apps/api/package.json
Adds @hey-api/openapi-ts dev dependency, @hypr/api-client workspace dependency, and new openapi npm script.
Desktop Onboarding State Machine
apps/desktop/src/components/onboarding/machine.ts, apps/desktop/src/routes/app/onboarding/index.tsx
Introduces xstate-based onboarding state machine with step progression logic, replaces in-component navigation with actor-driven event handling and lifecycle effects.
Desktop Onboarding Components
apps/desktop/src/components/onboarding/configure-notice.tsx, apps/desktop/src/components/onboarding/shared.tsx, apps/desktop/src/components/onboarding/login.tsx, apps/desktop/src/components/onboarding/config.tsx
Removes model-download feature, adds configuration notice component displayed on local login, extends OnboardingNext callback signature with optional step parameter, refactors login flow to auto-start trial eligibility checks and derive Pro entitlements.
Desktop Onboarding Removal
apps/desktop/src/components/onboarding/model.tsx
Deletes entire model download component, ModelCard, useModelDownload hook, and related state management.
Desktop Authentication & Utils
apps/desktop/src/auth.tsx, apps/desktop/src/utils/index.ts, apps/desktop/src/billing.tsx
Extracts getScheme() function to utils, updates refreshSession return type to `Promise<Session
Desktop Settings & Billing UI
apps/desktop/src/components/settings/account.tsx, apps/desktop/src/routes/app/settings/_layout.tsx
Replaces static plan status with dynamic BillingButton that queries trial eligibility and triggers trial/upgrade flows via API, adds disabled "data-import" tab.
Web Middleware & Configuration
apps/web/src/middleware/supabase.ts, apps/web/src/env.ts, apps/web/src/routes/_view/callback/auth.tsx
Removes supabaseClientMiddleware, refactors supabaseAuthMiddleware to use case-insensitive token parsing and locally constructed Supabase client, adds VITE_API_URL env variable, reduces desktop deeplink debounce from 2000ms to 200ms.
Web Documentation & UI
apps/web/content/docs/developers/8.api.mdx, apps/web/src/components/openapi-docs.tsx
Replaces API documentation with placeholder notice "Public API is coming soon", updates OpenAPI docs component endpoint reference from /openapi.json to /openapi.gen.json.
API Client Package
packages/api-client/package.json, packages/api-client/tsconfig.json
Establishes new workspace package for auto-generated API client with exports pointing to generated source, enables ESNext and strict TypeScript config.
VSCode & Task Configuration
.vscode/settings.json, Taskfile.yaml, plugins/webhook/src/lib.rs
Excludes generated/ directory in VSCode search, adds supabase-no-expose task variant, updates webhook test to write OpenAPI spec to openapi.gen.json.
Package Dependencies
apps/desktop/package.json, apps/web/package.json
Both add @hypr/api-client workspace dependency for API client integration.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Desktop as Desktop<br/>(Login)
    participant API as API Server
    participant Stripe as Stripe API
    participant DB as Supabase<br/>(DB)
    participant Auth as Supabase<br/>(Auth)

    User->>Desktop: Signs in
    Desktop->>Auth: Authenticate with callback
    Auth-->>Desktop: access_token + refresh_token
    
    rect rgb(200, 220, 255)
    Note over Desktop,DB: Post-Login Trial Check & Initialization
    end
    
    Desktop->>API: GET /rpc/can-start-trial
    API->>DB: RPC: can_start_trial()
    DB->>DB: Check user profile & subscriptions
    DB-->>API: canStartTrial: boolean
    API-->>Desktop: { canStartTrial }
    
    alt Trial Eligible
        Desktop->>API: POST /billing/start-trial
        API->>DB: Fetch stripe_customer_id from profiles
        
        alt No Stripe Customer
            API->>Stripe: Create customer
            Stripe-->>API: customer_id
            API->>DB: Update profiles with customer_id
        end
        
        API->>Stripe: Create subscription (14-day trial)
        Stripe-->>API: subscription created
        API-->>Desktop: { started: true }
        
        rect rgb(200, 255, 200)
        Note over Desktop: Refresh session & derive Pro entitlement
        end
        
        Desktop->>Auth: Refresh session
        Auth-->>Desktop: Updated access_token
        Desktop->>Desktop: Decode Pro entitlement from token
        Desktop->>Desktop: Set local: false (cloud mode)
    else Trial Not Eligible
        API-->>Desktop: { started: false }
        Desktop->>Desktop: Set local: true (local mode)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Key areas requiring focused attention:
    • apps/api/src/routes/billing.ts: Complex Stripe customer/subscription logic with error handling and trial eligibility checks
    • apps/desktop/src/components/onboarding/login.tsx: Significant refactor with new async post-login flow, trial checking, and session refresh logic spanning ~100+ lines
    • apps/desktop/src/components/onboarding/machine.ts: New xstate state machine with step progression; verify context merging and event handling logic
    • apps/desktop/src/routes/app/onboarding/index.tsx: Substantial refactor from component-driven to actor-driven flow; verify state transitions and URL sync
    • supabase/migrations/20250101000004_can_start_trial.sql: Verify RPC logic correctly checks subscription status and trial timing (3-month window)
    • apps/api/src/scripts/generate-openapi.ts: Verify script correctly parses tags and generates client with PRIVATE tag filtering
    • Cross-file middleware changes: Both API and web middleware updated with bearer token parsing; ensure consistency and regex correctness

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author, making it impossible to evaluate whether the description relates to the changeset. Add a pull request description that explains the free trial implementation, including the key changes made across the API, desktop, and web apps.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Implement Free Trial' directly summarizes the main objective of this comprehensive changeset, which adds trial-related features across multiple apps.

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a5dc5e and 37298be.

📒 Files selected for processing (3)
  • apps/api/src/routes/billing.ts (1 hunks)
  • apps/api/src/routes/rpc.ts (1 hunks)
  • apps/desktop/src/components/onboarding/configure-notice.tsx (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/env.ts (1)

28-38: Reconsider defaulting VITE_API_URL to the production host

Using default("https://api.hyprnote.com") means any environment without VITE_API_URL set (including local dev or CI) will silently talk to production, which is risky for data and billing flows. With emptyStringAsUndefined: true, even an empty var will fall back to prod. The skipValidation: isCI setting means CI won't catch a missing var either.

Compare this to apps/desktop/src/env.ts, which correctly defaults VITE_API_URL to "http://localhost:8787" for safe local development. There is no .env.sample to guide developers on required setup.

If you don't explicitly want that behavior, consider requiring the value instead and setting it per-env in .env files:

-    VITE_API_URL: z.string().default("https://api.hyprnote.com"),
+    VITE_API_URL: z.string().min(1),

or introduce a dev-only default that doesn't point at production.

🧹 Nitpick comments (13)
Taskfile.yaml (1)

94-98: LGTM!

The new supabase-no-expose task is correctly implemented. It provides a useful alternative workflow that updates Supabase environment variables without re-exposing the tunnel, complementing the existing supabase-expose task.

Optional: Extract shared commands to reduce duplication.

The first two commands (supabase-tunnel-stop and supabase-env) are duplicated between supabase-expose and supabase-no-expose. Consider extracting them into a shared task (e.g., supabase-env-update) to adhere to the DRY principle:

+  supabase-env-update:
+    platforms: [darwin]
+    cmds:
+      - task: supabase-tunnel-stop
+      - task: supabase-env
+
   supabase-expose:
     platforms: [darwin]
     cmds:
-      - task: supabase-tunnel-stop
-      - task: supabase-env
+      - task: supabase-env-update
       - task: supabase-tunnel-start
 
   supabase-no-expose:
     platforms: [darwin]
     cmds:
-      - task: supabase-tunnel-stop
-      - task: supabase-env
+      - task: supabase-env-update

This refactoring centralizes the shared workflow and makes it easier to maintain both tasks.

apps/desktop/src/components/settings/account.tsx (1)

226-236: Consider handling loading and error states for trial eligibility.

The canTrialQuery.data check doesn't account for loading or error states. Users might briefly see the "Upgrade to Pro" button while the trial eligibility check is pending.

+  if (canTrialQuery.isPending) {
+    return (
+      <Button variant="outline" disabled>
+        <span>Loading...</span>
+      </Button>
+    );
+  }
+
   if (canTrialQuery.data) {
     return (
       <Button
apps/desktop/src/components/onboarding/shared.tsx (1)

6-9: Consider using OnboardingStepId instead of string for better type safety.

The step field is typed as string, but it represents an OnboardingStepId. This requires a cast in machine.ts (line 107 in onboarding/index.tsx). Using the specific type would provide compile-time validation.

+import type { OnboardingStepId } from "./config";
+
 export type OnboardingNext = (params?: {
   local?: boolean;
-  step?: string;
+  step?: OnboardingStepId;
 }) => void;
apps/desktop/src/components/onboarding/login.tsx (2)

21-55: Missing cleanup for async operation may cause issues on unmount.

The async IIFE can complete after the component unmounts, potentially calling onNext on an unmounted component. Consider using an abort pattern:

 useEffect(() => {
   if (auth?.session && !trialStarted.current) {
     trialStarted.current = true;
+    let cancelled = false;

     const client = createClient(
       createConfig({
         baseUrl: env.VITE_API_URL,
         headers: { Authorization: `Bearer ${auth.session.access_token}` },
       }),
     );

     (async () => {
       try {
         const { data } = await getRpcCanStartTrial({ client });
         if (data?.canStartTrial) {
           await postBillingStartTrial({
             client,
             query: { interval: "monthly" },
           });
         }

         const newSession = await auth.refreshSession();
         const isPro = newSession
           ? getEntitlementsFromToken(newSession.access_token).includes(
               "hyprnote_pro",
             )
           : false;
+        if (!cancelled) onNext({ local: !isPro });
-        onNext({ local: !isPro });
       } catch (e) {
         console.error("Failed to process login:", e);
+        if (!cancelled) onNext({ local: true });
-        onNext({ local: true });
       }
     })();
+
+    return () => {
+      cancelled = true;
+    };
   }
 }, [auth?.session, auth?.refreshSession, onNext]);

Also note: users see "Waiting for sign in..." even after they're logged in while the trial logic runs. Consider adding a loading/processing state for better UX.


77-83: Consider validating callback URL before submission.

The Submit button calls handleAuthCallback without validating that callbackUrl is non-empty or contains expected tokens. This could lead to silent failures or confusing error states.

 <Button
-  onClick={() => auth?.handleAuthCallback(callbackUrl)}
+  onClick={() => callbackUrl.trim() && auth?.handleAuthCallback(callbackUrl)}
+  disabled={!callbackUrl.trim()}
   className="w-full"
 >
   Submit
 </Button>
apps/api/src/env.ts (1)

32-32: Consider the tradeoffs of skipping validation in CI.

While skipping environment validation in CI is a common pattern to allow for partial configurations during build/test, it could mask missing or misconfigured environment variables that would fail in production.

Consider whether CI jobs truly need to skip validation, or if test-specific environment files could be provided instead.

apps/web/content/docs/developers/8.api.mdx (1)

7-9: Consider adding a bit more guidance than a one-line placeholder

The “Public API is coming soon.” placeholder is fine short‑term, but given the page title/summary, you might want to add a brief note about current status (e.g., private/internal only) or link to any internal OpenAPI docs so developers aren’t left at a dead end.

apps/api/package.json (1)

6-11: Validate openapi script env behavior and @hypr/api-client dependency scope

The openapi script and OpenAPI tooling wiring look reasonable, but two things are worth double‑checking:

  • CI=true triggers skipValidation in your env setup; make sure src/scripts/generate-openapi.ts doesn’t rely on validated env vars (e.g., secrets) or, if it does, that they’re documented separately.
  • If @hypr/api-client is only used by the generator and/or for types, consider moving it to devDependencies to keep the API service’s runtime dependency set smaller.

[Suggest adjusting only if these assumptions hold.]

Also applies to: 13-27, 30-34

apps/api/src/middleware/load-test-auth.ts (1)

8-15: Case-insensitive Bearer handling improves load-test override robustness

Parsing the Authorization header with /^bearer /i is a nice improvement—this will accept any casing of Bearer while preserving existing behavior. If you expect headers with irregular spacing, you could further relax it to /^bearer\s+/i, but that’s purely optional for a load-test‑only path.

apps/api/src/scripts/generate-openapi.ts (2)

13-14: Consider consistent path resolution.

Line 13-14 uses new URL() with import.meta.url for output path, but line 19 uses a hardcoded relative string "./openapi.gen.json". Line 20 also uses a hardcoded relative path.

For consistency and reliability, consider using the same URL-based approach for all paths:

+  const inputPath = new URL("../../openapi.gen.json", import.meta.url).pathname;
+  const outputPath = new URL("../../packages/api-client/src/generated", import.meta.url).pathname;
+
   try {
     await createClient({
-      input: "./openapi.gen.json",
-      output: "../../packages/api-client/src/generated",
+      input: inputPath,
+      output: outputPath,
       parser: {

This ensures paths work correctly regardless of the working directory when the script is executed.

Also applies to: 19-20


30-33: Error handling terminates abruptly.

Line 32 uses process.exit(1) which immediately terminates the process without cleanup. While acceptable for build scripts, consider whether any cleanup is needed.

This is appropriate for a build script, but if you need to ensure cleanup (closing connections, flushing logs, etc.), consider:

} catch (error) {
  console.error("Failed to generate OpenAPI client:");
  console.error(error);
  // Perform any necessary cleanup here
  process.exit(1);
}

Alternatively, let the error propagate naturally and let the runtime handle it if no cleanup is needed.

apps/api/openapi.gen.json (1)

1-466: Verify security design is intentional.

The static analysis tools flag missing global security rules and suggest array constraints. While this is a generated file, consider:

  1. Security design: Some endpoints (health, billing, rpc) have no security requirements while others use Bearer auth. Verify this mixed approach aligns with your security model.

  2. Array validation: The messages array (lines 139-161) and similar arrays lack maxItems constraints. Consider adding limits in the source schemas to prevent abuse.

Do you want me to help identify where in the source code these constraints should be added?

apps/web/src/functions/billing.ts (1)

223-288: Consider extracting shared customer logic.

The customer retrieval/creation logic (lines 226-263) is duplicated from createCheckoutSession. Consider extracting this into a shared helper function to reduce duplication and improve maintainability.

Apply this refactor:

// Extract shared logic
const getOrCreateStripeCustomer = async (
  supabase: SupabaseClient,
  user: AuthUser,
  stripe: ReturnType<typeof getStripeClient>
) => {
  let stripeCustomerId = await getStripeCustomerIdForUser(supabase, user);

  if (!stripeCustomerId) {
    const newCustomer = await stripe.customers.create({
      email: user.email,
      metadata: { userId: user.id },
    });

    await Promise.all([
      supabase.auth.updateUser({
        data: { stripe_customer_id: newCustomer.id },
      }),
      supabase
        .from("profiles")
        .update({ stripe_customer_id: newCustomer.id })
        .eq("id", user.id),
    ]);

    stripeCustomerId = newCustomer.id;
  }

  return stripeCustomerId;
};

// Then use in both functions:
const stripeCustomerId = await getOrCreateStripeCustomer(
  supabase,
  { id: user.id, user_metadata: user.user_metadata },
  stripe
);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a04a15 and 9a5dc5e.

⛔ Files ignored due to path filters (14)
  • packages/api-client/src/generated/client.gen.ts is excluded by !**/generated/**, !**/generated/**, !**/*.gen.ts
  • packages/api-client/src/generated/client/client.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/client/index.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/client/types.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/client/utils.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/core/auth.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/core/bodySerializer.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/core/params.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/core/pathSerializer.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/core/types.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/index.ts is excluded by !**/generated/**, !**/generated/**
  • packages/api-client/src/generated/sdk.gen.ts is excluded by !**/generated/**, !**/generated/**, !**/*.gen.ts
  • packages/api-client/src/generated/types.gen.ts is excluded by !**/generated/**, !**/generated/**, !**/*.gen.ts
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (44)
  • .vscode/settings.json (1 hunks)
  • Taskfile.yaml (1 hunks)
  • apps/api/openapi.gen.json (1 hunks)
  • apps/api/package.json (2 hunks)
  • apps/api/src/env.ts (2 hunks)
  • apps/api/src/hono-bindings.ts (2 hunks)
  • apps/api/src/index.ts (2 hunks)
  • apps/api/src/middleware/load-test-auth.ts (1 hunks)
  • apps/api/src/middleware/supabase.ts (2 hunks)
  • apps/api/src/openapi.ts (1 hunks)
  • apps/api/src/routes/billing.ts (1 hunks)
  • apps/api/src/routes/constants.ts (1 hunks)
  • apps/api/src/routes/health.ts (1 hunks)
  • apps/api/src/routes/index.ts (2 hunks)
  • apps/api/src/routes/llm.ts (1 hunks)
  • apps/api/src/routes/rpc.ts (1 hunks)
  • apps/api/src/routes/stt.ts (2 hunks)
  • apps/api/src/routes/webhook.ts (1 hunks)
  • apps/api/src/scripts/generate-openapi.ts (1 hunks)
  • apps/desktop/package.json (1 hunks)
  • apps/desktop/src/auth.tsx (4 hunks)
  • apps/desktop/src/billing.tsx (2 hunks)
  • apps/desktop/src/components/onboarding/config.tsx (2 hunks)
  • apps/desktop/src/components/onboarding/configure-notice.tsx (1 hunks)
  • apps/desktop/src/components/onboarding/login.tsx (2 hunks)
  • apps/desktop/src/components/onboarding/machine.ts (1 hunks)
  • apps/desktop/src/components/onboarding/model.tsx (0 hunks)
  • apps/desktop/src/components/onboarding/shared.tsx (1 hunks)
  • apps/desktop/src/components/settings/account.tsx (2 hunks)
  • apps/desktop/src/routes/app/onboarding/index.tsx (2 hunks)
  • apps/desktop/src/routes/app/settings/_layout.tsx (3 hunks)
  • apps/desktop/src/utils/index.ts (1 hunks)
  • apps/web/content/docs/developers/8.api.mdx (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/components/openapi-docs.tsx (2 hunks)
  • apps/web/src/env.ts (1 hunks)
  • apps/web/src/functions/billing.ts (2 hunks)
  • apps/web/src/middleware/supabase.ts (3 hunks)
  • apps/web/src/routes/_view/app/account.tsx (5 hunks)
  • apps/web/src/routes/_view/callback/auth.tsx (1 hunks)
  • packages/api-client/package.json (1 hunks)
  • packages/api-client/tsconfig.json (1 hunks)
  • plugins/webhook/src/lib.rs (1 hunks)
  • supabase/migrations/20250101000004_can_start_trial.sql (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/desktop/src/components/onboarding/model.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/web/src/components/openapi-docs.tsx
  • apps/api/src/routes/llm.ts
  • apps/desktop/src/components/onboarding/shared.tsx
  • apps/api/src/middleware/supabase.ts
  • apps/api/src/routes/webhook.ts
  • apps/api/src/routes/billing.ts
  • apps/web/src/functions/billing.ts
  • apps/desktop/src/utils/index.ts
  • apps/api/src/routes/rpc.ts
  • apps/api/src/scripts/generate-openapi.ts
  • apps/api/src/hono-bindings.ts
  • apps/desktop/src/components/onboarding/configure-notice.tsx
  • apps/api/src/routes/health.ts
  • apps/api/src/routes/stt.ts
  • apps/desktop/src/components/onboarding/machine.ts
  • apps/api/src/routes/index.ts
  • apps/web/src/routes/_view/app/account.tsx
  • apps/web/src/routes/_view/callback/auth.tsx
  • apps/desktop/src/routes/app/onboarding/index.tsx
  • apps/api/src/middleware/load-test-auth.ts
  • apps/desktop/src/components/onboarding/config.tsx
  • apps/desktop/src/components/onboarding/login.tsx
  • apps/api/src/env.ts
  • apps/desktop/src/components/settings/account.tsx
  • apps/api/src/routes/constants.ts
  • apps/api/src/index.ts
  • apps/api/src/openapi.ts
  • apps/web/src/env.ts
  • apps/desktop/src/auth.tsx
  • apps/desktop/src/routes/app/settings/_layout.tsx
  • apps/desktop/src/billing.tsx
  • apps/web/src/middleware/supabase.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Agent implementations should use TypeScript and follow the established architectural patterns defined in the agent framework
Agent communication should use defined message protocols and interfaces

Files:

  • apps/api/src/routes/llm.ts
  • apps/api/src/middleware/supabase.ts
  • apps/api/src/routes/webhook.ts
  • apps/api/src/routes/billing.ts
  • apps/web/src/functions/billing.ts
  • apps/desktop/src/utils/index.ts
  • apps/api/src/routes/rpc.ts
  • apps/api/src/scripts/generate-openapi.ts
  • apps/api/src/hono-bindings.ts
  • apps/api/src/routes/health.ts
  • apps/api/src/routes/stt.ts
  • apps/desktop/src/components/onboarding/machine.ts
  • apps/api/src/routes/index.ts
  • apps/api/src/middleware/load-test-auth.ts
  • apps/api/src/env.ts
  • apps/api/src/routes/constants.ts
  • apps/api/src/index.ts
  • apps/api/src/openapi.ts
  • apps/web/src/env.ts
  • apps/web/src/middleware/supabase.ts
plugins/*/src/lib.rs

📄 CodeRabbit inference engine (plugins/AGENTS.md)

After updating commands in plugins/<NAME>/src/lib.rs, run codegen, update plugins/<NAME>/permissions/default.toml, and apps/desktop/src-tauri/capabilities/default.json

Files:

  • plugins/webhook/src/lib.rs
🧠 Learnings (4)
📚 Learning: 2025-11-24T16:32:13.593Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: packages/nango/.cursor/rules/nango.mdc:0-0
Timestamp: 2025-11-24T16:32:13.593Z
Learning: Applies to packages/nango/**/*.ts : API endpoints must be correctly implemented according to actual API documentation

Applied to files:

  • apps/web/src/components/openapi-docs.tsx
  • apps/api/src/index.ts
📚 Learning: 2025-11-24T16:32:13.593Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: packages/nango/.cursor/rules/nango.mdc:0-0
Timestamp: 2025-11-24T16:32:13.593Z
Learning: Applies to packages/nango/**/models.ts : Do NOT edit the models.ts file - it is automatically generated at compilation time

Applied to files:

  • packages/api-client/tsconfig.json
  • .vscode/settings.json
📚 Learning: 2025-11-27T11:40:22.782Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: plugins/AGENTS.md:0-0
Timestamp: 2025-11-27T11:40:22.782Z
Learning: Applies to plugins/*/src/lib.rs : After updating commands in `plugins/<NAME>/src/lib.rs`, run `codegen`, update `plugins/<NAME>/permissions/default.toml`, and `apps/desktop/src-tauri/capabilities/default.json`

Applied to files:

  • plugins/webhook/src/lib.rs
📚 Learning: 2025-11-24T16:32:13.593Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: packages/nango/.cursor/rules/nango.mdc:0-0
Timestamp: 2025-11-24T16:32:13.593Z
Learning: Applies to packages/nango/**/*.ts : Do NOT set Authorization header - Nango handles this automatically

Applied to files:

  • apps/api/src/middleware/load-test-auth.ts
🧬 Code graph analysis (19)
apps/api/src/routes/llm.ts (2)
apps/api/src/routes/constants.ts (1)
  • API_TAGS (1-5)
apps/api/src/routes/index.ts (1)
  • API_TAGS (11-11)
apps/api/src/routes/webhook.ts (1)
apps/api/src/routes/constants.ts (1)
  • API_TAGS (1-5)
apps/web/src/functions/billing.ts (4)
apps/web/src/functions/supabase.ts (1)
  • getSupabaseServerClient (20-36)
apps/web/src/env.ts (1)
  • env (7-43)
apps/api/src/env.ts (1)
  • env (4-33)
apps/web/src/functions/stripe.ts (1)
  • getStripeClient (6-10)
apps/api/src/routes/rpc.ts (3)
apps/api/src/hono-bindings.ts (1)
  • AppBindings (7-17)
apps/api/src/routes/constants.ts (1)
  • API_TAGS (1-5)
apps/web/src/middleware/supabase.ts (1)
  • supabaseAuthMiddleware (6-39)
apps/api/src/scripts/generate-openapi.ts (3)
apps/api/src/routes/index.ts (2)
  • routes (13-13)
  • API_TAGS (11-11)
apps/api/src/openapi.ts (1)
  • openAPIDocumentation (3-28)
apps/api/src/routes/constants.ts (1)
  • API_TAGS (1-5)
apps/desktop/src/components/onboarding/configure-notice.tsx (2)
apps/desktop/src/components/onboarding/shared.tsx (1)
  • OnboardingNext (6-9)
extensions/shared/types/hypr-extension.d.ts (1)
  • Button (159-159)
apps/api/src/routes/health.ts (1)
apps/api/src/routes/constants.ts (1)
  • API_TAGS (1-5)
apps/api/src/routes/stt.ts (2)
apps/api/src/routes/constants.ts (1)
  • API_TAGS (1-5)
apps/api/src/routes/index.ts (1)
  • API_TAGS (11-11)
apps/desktop/src/components/onboarding/machine.ts (1)
apps/desktop/src/components/onboarding/config.tsx (3)
  • OnboardingStepId (13-17)
  • OnboardingContext (19-27)
  • STEP_CONFIGS (35-56)
apps/api/src/routes/index.ts (3)
apps/api/src/routes/billing.ts (1)
  • billing (20-20)
apps/api/src/routes/llm.ts (1)
  • llm (36-36)
apps/api/src/routes/rpc.ts (1)
  • rpc (14-14)
apps/web/src/routes/_view/app/account.tsx (1)
apps/web/src/functions/billing.ts (2)
  • canStartTrial (196-221)
  • createTrialCheckoutSession (223-288)
apps/desktop/src/routes/app/onboarding/index.tsx (3)
apps/desktop/src/components/onboarding/config.tsx (3)
  • OnboardingContext (19-27)
  • OnboardingStepId (13-17)
  • STEP_CONFIGS (35-56)
apps/desktop/src/components/onboarding/machine.ts (2)
  • createOnboardingLogic (28-50)
  • OnboardingState (9-13)
apps/desktop/src/components/onboarding/shared.tsx (1)
  • OnboardingNext (6-9)
apps/desktop/src/components/onboarding/config.tsx (1)
apps/desktop/src/components/onboarding/configure-notice.tsx (1)
  • ConfigureNotice (9-45)
apps/desktop/src/components/onboarding/login.tsx (4)
apps/desktop/src/components/onboarding/shared.tsx (2)
  • OnboardingNext (6-9)
  • OnboardingContainer (24-51)
apps/desktop/src/auth.tsx (1)
  • useAuth (299-307)
apps/desktop/src/billing.tsx (1)
  • getEntitlementsFromToken (14-21)
extensions/shared/types/hypr-extension.d.ts (2)
  • Input (322-322)
  • Button (159-159)
apps/desktop/src/components/settings/account.tsx (3)
apps/desktop/src/auth.tsx (1)
  • useAuth (299-307)
apps/desktop/src/billing.tsx (1)
  • useBillingAccess (66-74)
apps/desktop/src/env.ts (1)
  • env (4-17)
apps/api/src/index.ts (2)
apps/api/src/routes/index.ts (1)
  • routes (13-13)
apps/api/src/openapi.ts (1)
  • openAPIDocumentation (3-28)
apps/api/src/openapi.ts (1)
apps/api/src/routes/constants.ts (1)
  • API_TAGS (1-5)
apps/desktop/src/auth.tsx (1)
packages/store/src/schema-external.ts (1)
  • Session (169-169)
apps/web/src/middleware/supabase.ts (2)
apps/api/src/middleware/supabase.ts (1)
  • supabaseAuthMiddleware (7-33)
apps/web/src/env.ts (1)
  • env (7-43)
🪛 Checkov (3.2.334)
apps/api/openapi.gen.json

[high] 1-465: Ensure that the global security field has rules defined

(CKV_OPENAPI_4)


[high] 1-465: Ensure that security operations is not empty.

(CKV_OPENAPI_5)


[medium] 139-161: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: desktop_ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: desktop_ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: desktop_ci (macos, depot-macos-14)
  • GitHub Check: tests
  • GitHub Check: ci
  • GitHub Check: fmt
🔇 Additional comments (48)
apps/desktop/src/routes/app/settings/_layout.tsx (1)

7-14: New “Data Import” tab wiring looks consistent and safe while disabled

The Import icon import, "data-import" addition to TAB_KEYS, and its TAB_CONFIG entry are all consistent with the existing pattern. Because it’s marked disabled: true, it won’t appear in enabledTabs or the sidebar, but remains a valid TabKey and search param for future use. No issues from a routing, typing, or UX perspective.

Also applies to: 23-34, 73-78

apps/web/src/routes/_view/callback/auth.tsx (1)

87-100: Shorter desktop redirect delay looks good

Changing the timeout to 200ms should make the desktop deeplink feel more responsive, and it doesn’t introduce any new correctness issues given the current flow and dependencies.

apps/desktop/src/billing.tsx (2)

14-21: Clean refactoring that improves code organization.

The extracted helper function makes the code more maintainable and reusable. The error handling correctly returns a safe default on JWT decode failure.


40-40: LGTM!

Correct usage of the helper function. The early return check for missing tokens (lines 37-39) ensures the helper receives a valid token.

apps/desktop/src/utils/index.ts (1)

8-17: LGTM!

The getScheme function is well-structured with a clear mapping and sensible default fallback. Minor note: the local id variable shadows the exported id function in this module, but since they're in different scopes this doesn't cause issues.

apps/desktop/src/auth.tsx (5)

23-23: LGTM!

Clean refactor to use the centralized getScheme utility instead of inline implementation.


25-68: LGTM!

Good defensive programming with iframe context detection and conditional Tauri storage creation. The isLocalAuthServer helper properly handles URL parsing errors, and clearAuthStorage appropriately silences errors during cleanup operations.


70-88: LGTM!

The conditional Supabase client creation correctly guards against iframe contexts where Tauri APIs are unavailable. The null-check chain ensures all required configuration is present.


95-95: LGTM!

The updated return type Promise<Session | null> is more informative for callers who need to act on the refreshed session.


244-258: LGTM!

The refreshSession implementation correctly returns the session when available and null on failure, aligning with the updated type signature.

apps/desktop/src/components/onboarding/config.tsx (1)

13-17: LGTM!

The step configuration update correctly introduces configure-notice as a local-only step, with proper shouldShow predicate that displays this step when the user is in local mode.

Also applies to: 46-50

apps/desktop/src/components/settings/account.tsx (2)

183-203: Clarify the purpose of the artificial delay.

The 3-second setTimeout delay (line 198) before mutation completion is unclear. If this is waiting for backend state propagation (e.g., Stripe webhook processing), consider:

  1. Adding a comment explaining the rationale
  2. Polling for the actual state change instead of a fixed delay
  3. Handling cases where 3 seconds isn't sufficient
     mutationFn: async () => {
       const headers = auth?.getHeaders();
       if (!headers) {
         throw new Error("Not authenticated");
       }
       const client = createClient({ baseUrl: env.VITE_API_URL, headers });
       const { error } = await postBillingStartTrial({
         client,
         query: { interval: "monthly" },
       });
       if (error) {
         throw error;
       }
 
+      // Wait for Stripe webhook to update subscription status
       await new Promise((resolve) => setTimeout(resolve, 3000));
     },

165-181: LGTM!

Good use of useQuery with proper query key structure including user ID for cache isolation. The enabled flag correctly prevents queries when unauthenticated or already Pro.

apps/desktop/src/components/onboarding/configure-notice.tsx (1)

9-44: LGTM!

The ConfigureNotice component is well-structured with clear navigation options. The button actions correctly use the onNext callback with appropriate parameters for both the sign-up redirect and the continue flow.

apps/desktop/src/components/onboarding/machine.ts (2)

19-26: Verify behavior when current step is not in visible steps.

If currentStep is not found in visibleSteps, findIndex returns -1, and visibleSteps[-1 + 1] returns visibleSteps[0], effectively restarting from the first visible step. Verify this is the intended fallback behavior.


28-49: LGTM!

The state machine implementation using XState's fromTransition is clean and handles both GO_TO (explicit navigation) and NEXT (sequential progression) events correctly. The context merging with local override is well-designed.

apps/desktop/src/routes/app/onboarding/index.tsx (4)

36-41: LGTM!

The finishOnboarding function correctly sequences the window operations—showing the main window before destroying the onboarding window ensures a smooth transition.


85-100: LGTM!

The effect correctly uses a ref to track previous state and only triggers navigation when step or local actually changes. The early return on state.done prevents unnecessary navigation after completion.


102-115: LGTM!

The goNext callback correctly dispatches GO_TO for explicit step navigation and NEXT for sequential progression, aligning with the state machine's event handling.


81-83: The useSelector usage with fromTransition is correct; no changes needed.

XState v5's fromTransition creates actor snapshots with a context property that holds the reducer's return value. The snapshot structure is { status, output, error, ... context: TContext }, so accessing s.context in the selector is the correct pattern. This is confirmed by the existing usage in chat.ts line 50 where snapshot.context is accessed in the same way.

apps/desktop/src/components/onboarding/login.tsx (2)

57-62: LGTM!

The ref guard pattern correctly ensures sign-in is only initiated once on mount.


96-126: LGTM!

The waiting UI provides clear options for troubleshooting sign-in issues and correctly uses OnboardingContainer for consistent styling.

plugins/webhook/src/lib.rs (1)

64-64: LGTM! Filename aligns with generated file conventions.

The change to openapi.gen.json is consistent with the naming pattern for generated files used throughout the PR.

.vscode/settings.json (1)

8-9: LGTM! Appropriate exclusion for generated code.

Excluding the generated/ directory from search results improves developer experience by hiding auto-generated files.

packages/api-client/tsconfig.json (1)

1-13: LGTM! Well-configured for a generated API client.

The TypeScript configuration appropriately enables strict mode, declaration generation, and uses modern ESNext with bundler resolution.

supabase/migrations/20250101000004_can_start_trial.sql (2)

1-36: Well-implemented security-definer function with proper privilege control.

The function correctly uses SECURITY DEFINER with search_path = '' to prevent schema injection attacks, and properly restricts access to authenticated users only.


24-34: The trial_start extraction could be clearer.

The stripe.subscriptions.trial_start column stores JSONB data containing Unix timestamps from the Stripe API (numeric values). The expression (trial_start #>> '{}')::bigint works correctly—#>>'{} extracts the JSON scalar as text, and ::bigint converts it to a number. However, this operator choice is unconventional for scalar values. Consider using (trial_start::text)::bigint or a direct cast for improved readability.

apps/desktop/package.json (1)

18-18: LGTM! Standard workspace dependency addition.

The new @hypr/api-client dependency enables the desktop app to consume the generated API client for billing and trial functionality.

apps/web/package.json (1)

15-15: LGTM! Standard workspace dependency addition.

The new @hypr/api-client dependency enables the web app to consume the generated API client for billing and trial functionality.

apps/api/src/middleware/supabase.ts (2)

14-14: Good improvement: case-insensitive Bearer token parsing.

Using a case-insensitive regex makes the middleware more robust against client implementations that may send "bearer", "Bearer", or "BEARER".


30-30: Good improvement: context-sharing the Supabase client.

Storing the authenticated Supabase client in the request context allows downstream routes to reuse it without recreating clients, improving efficiency and maintaining consistent authentication context.

apps/web/src/components/openapi-docs.tsx (1)

71-85: Update of OpenAPI spec path is consistent and safe

Switching both the fetch URL and the “View raw OpenAPI spec” link to /openapi.gen.json keeps the UI in sync with the new generated spec location; no issues from this change as long as the API serves that path in all environments.

Also applies to: 133-140

apps/api/src/routes/index.ts (1)

4-9: Billing and RPC routers are correctly mounted

Importing billing and rpc and routing them under /billing and /rpc matches the existing Hono composition pattern in this file; the wiring looks correct.

Also applies to: 15-20

packages/api-client/package.json (1)

1-7: API client package exports align with generated layout

The new @hypr/api-client manifest cleanly exposes the generated root and /client entrypoints. Just ensure generate-openapi.ts writes to src/generated/index.ts and src/generated/client/index.ts as declared here; you can add fields like type, version, and license later if/when you plan to publish outside the monorepo.

apps/api/src/routes/llm.ts (1)

39-47: /completions marked as private matches intent to keep LLM route internal

Switching the tag to API_TAGS.PRIVATE_SKIP_OPENAPI and simplifying the 200/401 descriptions makes sense if this endpoint is meant to stay off the public OpenAPI surface while remaining functional. Just confirm your OpenAPI generation pipeline actually filters out PRIVATE_SKIP_OPENAPI‑tagged routes so this doesn’t accidentally leak into the public spec.

apps/api/src/hono-bindings.ts (1)

2-2: LGTM!

The addition of the SupabaseClient type to AppBindings.Variables is correct and enables type-safe access to the Supabase client in downstream routes via the Hono context.

Also applies to: 14-14

apps/api/src/index.ts (1)

21-22: LGTM!

The refactoring centralizes OpenAPI documentation configuration, making it easier to maintain. The endpoint path change from "/openapi.json" to "/openapi.gen.json" correctly reflects that the spec is now generated, and all references are consistently updated.

Also applies to: 72-73, 81-81

apps/api/src/routes/webhook.ts (1)

22-22: LGTM!

The changes simplify OpenAPI metadata by using the new PRIVATE_SKIP_OPENAPI tag and generic descriptions. This aligns with the centralized OpenAPI documentation approach while preserving runtime behavior.

Also applies to: 25-25, 32-33

apps/web/src/routes/_view/app/account.tsx (1)

109-119: Verify boolean check logic for trial eligibility.

Line 109 checks if (canTrialQuery.data) to determine trial eligibility. If canStartTrial() legitimately returns false (user not eligible), this condition would correctly hide the button. However, it's worth confirming this is the intended behavior and not confusing false with loading/error states.

Verify that canStartTrial() in apps/web/src/functions/billing.ts returns:

  • true when eligible
  • false when not eligible or on error

If the function can return undefined, the check should be:

if (canTrialQuery.data === true) {
apps/api/src/routes/health.ts (1)

18-18: LGTM!

The changes update OpenAPI metadata to use the new PRIVATE_SKIP_OPENAPI tag, aligning with the broader API documentation restructuring. Runtime behavior is unchanged.

Also applies to: 21-21

apps/api/src/routes/constants.ts (1)

1-5: LGTM!

The refactored API_TAGS constant provides clearer semantics with PRIVATE_SKIP_OPENAPI for endpoints excluded from OpenAPI docs and PRIVATE for documented internal endpoints. The type narrowing with as const is appropriate.

apps/api/src/openapi.ts (1)

1-28: LGTM!

The centralized OpenAPI documentation structure is well-organized. Note that PRIVATE_SKIP_OPENAPI is correctly excluded from the tags array since those endpoints should not appear in the generated documentation.

apps/api/src/routes/stt.ts (2)

44-52: LGTM!

The tag change to PRIVATE_SKIP_OPENAPI appropriately excludes this WebSocket endpoint from OpenAPI documentation, and the simplified response descriptions are consistent with this intent.


62-77: LGTM!

Consistent with the /listen endpoint changes, the tag update and simplified descriptions appropriately handle this private STT endpoint while maintaining the response schema for type safety.

apps/web/src/functions/billing.ts (2)

196-221: LGTM!

The canStartTrial function properly validates the session, handles errors gracefully by logging and returning false, and uses nullish coalescing to ensure a boolean result.


265-285: LGTM! Trial configuration is correct.

The Stripe checkout session correctly implements a 14-day trial with:

  • payment_method_collection: "if_required" (allows trial start without payment)
  • missing_payment_method: "cancel" (safe default)
  • Monthly subscription only (appears intentional to simplify trial offering)
apps/web/src/middleware/supabase.ts (2)

8-19: LGTM!

The case-insensitive token extraction using /^bearer /i is more robust than the previous approach and properly handles various capitalizations.


21-37: LGTM!

The middleware correctly:

  1. Creates a per-request Supabase client with the Authorization header in global config
  2. Strips "bearer " prefix and re-adds "Bearer " for proper header format
  3. Passes both the client and user in context for downstream usage

This pattern aligns with the API middleware implementation in apps/api/src/middleware/supabase.ts.

@yujonglee yujonglee merged commit aaf7d51 into main Dec 10, 2025
15 of 18 checks passed
@yujonglee yujonglee deleted the trial branch December 10, 2025 05:44
@coderabbitai coderabbitai bot mentioned this pull request Dec 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant