Skip to content

Feat/migrate to digitalocean - #77

Merged
Mayank-saraswal merged 7 commits into
mainfrom
feat/migrate-to-digitalocean
Jun 3, 2026
Merged

Feat/migrate to digitalocean#77
Mayank-saraswal merged 7 commits into
mainfrom
feat/migrate-to-digitalocean

Conversation

@Mayank-saraswal

@Mayank-saraswal Mayank-saraswal commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Email verification required for new account signups
    • Razorpay-based subscription system with Starter, Pro, and Team billing tiers
    • Dedicated pricing page showcasing plan features and monthly/yearly billing options
    • Subscription management capabilities: view billing history, upgrade/downgrade plans, cancel subscriptions
    • Workflow run quota tracking per plan with monthly resets
  • Infrastructure

    • Cloud storage migrated to DigitalOcean Spaces for file uploads
    • Enhanced SEO with sitemap generation and structured data markup
    • Web app manifest for improved mobile experience

- Replace @azure/storage-blob with @aws-sdk/client-s3 + s3-request-presigner
- Rewrite media-service.ts for DigitalOcean Spaces (S3-compatible)
- Add new CI/CD workflow: deploy-digitalocean.yml
- Remove old Azure ACR + Container Apps deploy workflows
- Update env vars: AZURE_STORAGE_* -> DO_SPACES_*
- Add serverExternalPackages for AWS SDK v3 Turbopack compat
- Update all Azure references in source to cloud storage/DO Spaces
- Remove Azure Container Apps URL from auth.ts trusted origins
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Mayank-saraswal, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 46 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 52724922-90d5-43ff-bc76-3a5e166e9e47

📥 Commits

Reviewing files that changed from the base of the PR and between c54a7bf and fe3a8fa.

📒 Files selected for processing (23)
  • .github/workflows/deploy-digitalocean.yml
  • next-sitemap.config.js
  • public/manifest.json
  • public/robots.txt
  • public/sitemap.xml
  • scripts/create-razorpay-plans.ts
  • src/app/(auth)/check-email/page.tsx
  • src/app/(marketing)/pricing/pricing-page.tsx
  • src/app/api/auth/custom-signup/route.ts
  • src/app/api/webhooks/razorpay-billing/route.ts
  • src/app/layout.tsx
  • src/components/structured-data.tsx
  • src/config/pricing.ts
  • src/features/auth/components/register-form.tsx
  • src/features/executions/components/ai/executor.ts
  • src/features/executions/components/gmail/executor.ts
  • src/features/executions/components/media-upload/executor.ts
  • src/features/executions/components/postgres/__tests__/query-builder.test.ts
  • src/features/executions/components/postgres/executor.ts
  • src/features/executions/components/whatsapp/executor.ts
  • src/hooks/use-razorpay.ts
  • src/lib/email-verification.ts
  • src/lib/media-service.ts
📝 Walkthrough

Walkthrough

This PR migrates Nodebase from Azure Container Apps + Polar billing to DigitalOcean App Platform + Razorpay subscriptions, introduces email verification for user signup, implements plan-based workflow run quotas, adds a customer-facing pricing page, and refreshes branding and SEO metadata.

Changes

Infrastructure & Billing Foundation

Layer / File(s) Summary
DigitalOcean deployment and environment setup
.env.example, .github/workflows/deploy-digitalocean.yml, Dockerfile, next.config.ts, src/lib/auth.ts, src/lib/auth-client.ts
Replace Azure Container Apps CI/CD with DigitalOcean App Platform workflow, add DigitalOcean Spaces environment variables, update trusted origins and remove Polar plugin from auth configuration.
Media storage: Azure Blob → DigitalOcean Spaces
src/lib/media-service.ts, src/features/executions/components/ai/executor.ts, src/features/executions/components/gmail/executor.ts, src/features/executions/components/media-upload/dialog.tsx, src/features/executions/components/whatsapp/executor.ts
Switch from Azure Blob Storage to S3-compatible DigitalOcean Spaces for media upload, download with presigned URLs, and batch deletion via S3 commands; update executor comments to reference cloud storage instead of Azure.
Plan limits and Razorpay SDK initialization
src/lib/plan-limits.ts, src/lib/razorpay-billing.ts, src/lib/polar.ts, package.json, scripts/create-razorpay-plans.ts
Define Starter/Pro/Team plan quotas and names, lazily initialize Razorpay SDK from environment variables, remove Polar client, add AWS S3 and Razorpay dependencies, and provide setup script for creating Razorpay billing plans.

Email Verification & Signup Flow

Layer / File(s) Summary
Email verification token and SMTP utilities
src/lib/email-verification.ts
Generate secure verification tokens, compute 24-hour expiry times, and send SMTP-based verification emails with HTML and plain-text templates.
Custom signup, verify, and resend verification API routes
src/app/api/auth/custom-signup/route.ts, src/app/api/auth/verify-email/route.ts, src/app/api/auth/resend-verification/route.ts
Implement /api/auth/custom-signup with bcrypt password hashing and user creation; /api/auth/verify-email with token and expiry validation; /api/auth/resend-verification with 3-attempt rate limiting and email enumeration mitigation.
Signup, verification, and resend-verification pages
src/app/(auth)/check-email/page.tsx, src/app/(auth)/verify-email/page.tsx, src/app/(auth)/resend-verification/page.tsx
Create check-email page showing verification status, verify-email page handling token validation and success/error states, and resend-verification page with form submission and success messaging.
Login form email verification detection and resend link
src/features/auth/components/login-form.tsx
Detect unverified email login failures via message or HTTP 403, show toast with resend link, and render email-verified banner when query param present.
Register form custom signup integration
src/features/auth/components/register-form.tsx
Replace built-in email signup with /api/auth/custom-signup fetch, parse response, and redirect to check-email page on success.

Razorpay Billing System

Layer / File(s) Summary
Database schema: email verification and billing tables
prisma/schema.prisma, prisma/migrations/20260409154001_razorpay/migration.sql, prisma/migrations/20260409_add_razorpay_billing/migration.sql
Add email verification fields (emailVerifyToken, emailVerifyExpiry, emailVerifyAttempts) to User; add Razorpay subscription fields (plan, planStatus, razorpayCustomerId, razorpaySubId, currentPeriodEnd, cancelAtPeriodEnd, workflowRunsUsed, workflowRunsReset); create BillingEvent table for webhook audit logs with user FK and indexes on userId and type.
Billing service: subscription and quota management
src/lib/billing.ts
Provide helpers for fetching/creating Razorpay customers, creating subscriptions with short URL checkout, cancelling subscriptions, computing monthly quota with reset logic, and incrementing run count.
Execution gate: quota enforcement and plan-based limits
src/lib/execution-gate.ts
Replace Polar-based gating with plan-based monthly quota enforcement, support monthly reset based on workflowRunsReset, and always increment workflowRunsUsed on execution.
Razorpay webhook processing for billing events
src/app/api/webhooks/razorpay-billing/route.ts
Implement HMAC-SHA256 signature verification and handle subscription lifecycle events (activated, charged, failed, cancelled, paused, resumed), updating user plan/status and creating BillingEvent records for audit trail.
Razorpay checkout initiation hook
src/hooks/use-razorpay.ts
Create useRazorpay hook that lazily loads checkout.js, exposes openSubscriptionCheckout to initialize checkout with subscription/user/plan details, and handles payment success/failure callbacks.
TRPC billing router with authenticated endpoints
src/server/routers/billing.router.ts
Expose getStatus (current plan and quota), createSubscription (blocking duplicates, returning checkout details), cancelSubscription (immediate or period-end modes), getHistory (24 recent billing events), and checkQuota (quota info) endpoints.
TRPC premium procedure: plan-based access control
src/trpc/init.ts
Replace Polar premium checks with plan-based authorization via Prisma, verifying plan !== "FREE" and planStatus === "active", and passing userPlan into context.
TRPC router migration from usage to billing
src/trpc/routers/_app.ts
Remove usageRouter and register billingRouter in appRouter.
Subscription hook: migrate from Polar to TRPC billing
src/features/auth/components/subscriptions/hooks/use-subscription.ts
Update useSubscription to use trpc.billing.getStatus.queryOptions(); update useHasActiveSubscription to derive status from plan !== "FREE" and planStatus === "active"; return plan alongside subscription state.

Customer-Facing Pricing & Billing UI

Layer / File(s) Summary
Pricing page with tiers, features, and FAQ
src/app/(marketing)/pricing/page.tsx, src/app/(marketing)/pricing/pricing-page.tsx
Create /pricing route with Starter/Pro/Team cards supporting monthly/yearly toggle, display per-plan features and CTAs, render FAQ section, include hero and bottom CTA panel.
Upgrade modals and prompts routing to pricing
src/components/upgrade-modal.tsx, src/components/upgrade-prompt.tsx, src/components/app-sidebar.tsx, src/components/landing/marketing-page.tsx
Update upgrade and billing buttons to route to /pricing instead of invoking authClient checkout, change CTA text to "View Plans" and "Upgrade Plan", update marketing page pricing tier data with new plan structure.
Usage banner: show quota from billing status
src/components/usage-banner.tsx, src/features/workflows/hooks/use-workflows.ts
Switch from trpc.usage.getMyUsage to trpc.billing.getStatus, display plan badge for active subscriptions, show quota progress for free/inactive users, render {used}/{limit} runs counter, route upgrade CTA to /pricing.

SEO, Branding & Theme

Layer / File(s) Summary
Structured data: SoftwareApp and Organization schemas
src/components/structured-data.tsx, src/app/layout.tsx
Create SoftwareAppStructuredData and OrganizationStructuredData components that inject Schema.org JSON-LD into page head for SEO, describing app offerings, features, creators, address, and social links.
Sitemap generation and site metadata
next-sitemap.config.js, src/app/layout.tsx
Add next-sitemap configuration with robots.txt generation and route exclusions, update site metadata with "Nodebase — Workflow Automation Built for India" branding, set metadataBase URL, update OpenGraph and Twitter card fields.
Web app manifest and Open Graph image
public/manifest.json, src/app/opengraph-image.tsx
Create web app manifest with app metadata and icon paths; generate 1200×630 Open Graph image via ImageResponse with Nodebase branding text.
Dark mode theme variable and marketing styling updates
src/app/globals.css
Update .dark CSS palette to new oklch() values for backgrounds, cards, borders, and charts; add dark-mode image utilities; enable smooth scrolling; update marketing component hover states.
Empty view styling, node selector text, and minor updates
src/components/entity-components.tsx, src/components/node-selector.tsx, src/inngest/functions/schedule-poller.ts, src/features/triggers/components/google-form-trigger/executor.ts, src/features/triggers/components/manual-trigger/executor.ts, src/features/triggers/components/stripe-trigger/executor.ts
Update EmptyView background color to bg-background, change media upload description from Azure to cloud storage, adjust schedule-poller cron to daily 09:00, clean up unused retry imports from trigger executors.

Sequence Diagram

sequenceDiagram
    participant User
    participant App as Next.js App
    participant CustomSignup as /api/auth/custom-signup
    participant SMTP as Email Service
    participant VerifyEmail as /api/auth/verify-email
    participant Login as /login
    participant Pricing as /pricing
    participant Razorpay as Razorpay Checkout
    participant Webhook as /webhooks/razorpay-billing
    participant Prisma as Prisma (DB)
    
    User->>App: Register with email/password
    App->>CustomSignup: POST email, password, name
    CustomSignup->>Prisma: Create user + account records
    CustomSignup->>SMTP: Send verification email
    SMTP-->>User: Verification email with token link
    
    User->>VerifyEmail: Click verification link
    VerifyEmail->>Prisma: Mark emailVerified=true, clear token
    VerifyEmail-->>User: Redirect to login with verified=true
    
    User->>Login: Email + password
    Login->>Prisma: Authenticate user
    Prisma-->>Login: Return user + session
    Login-->>User: Redirect to dashboard
    
    User->>App: Click Upgrade / Billing
    App->>Pricing: Navigate to pricing page
    Pricing-->>User: Display Starter/Pro/Team cards
    
    User->>Pricing: Select plan
    Pricing->>Razorpay: openSubscriptionCheckout
    Razorpay-->>User: Checkout modal
    User->>Razorpay: Complete payment
    Razorpay->>Webhook: Send subscription.activated event
    Webhook->>Prisma: Update user plan, planStatus, razorpaySubId
    Webhook->>Prisma: Create BillingEvent record
    Webhook-->>Razorpay: Return 200 OK
    
    User->>App: Execute workflow
    App->>App: checkExecutionLimit via execution-gate
    App->>Prisma: Check workflowRunsUsed vs plan limit
    Prisma-->>App: Return allowed/quota
    App->>App: Run workflow
    App->>Prisma: incrementRunCount
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 From Azure clouds to DigitalOcean's shore,
Polar's checkout leaves, Razorpay's here to explore,
Email tokens sing, quotas dance with the plan,
Pricing pages gleam bright—oh what a span!
Workflow runs counted, billing bells ring anew,
Built for India, this migration's true! 🌟

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/migrate-to-digitalocean

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c54a7bf25e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

name: "Schedule Trigger Poller",
},
{ cron: "* * * * *" },
{ cron: "0 9 * * *" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore minute-level schedule polling

With the poller now running only at 0 9 * * *, the secondsSincePrev <= 60 gate below only fires schedules whose previous cron occurrence was within the minute when the daily poll happens. Any workflow scheduled for another minute/hour (and even * * * * *, except the 09:00 occurrence) will be skipped indefinitely, so existing schedule triggers effectively stop working outside that one daily minute.

Useful? React with 👍 / 👎.

Comment on lines +41 to +42
// 2. Hash password
const hashedPassword = await hash(password, 12)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Hash custom signups with Better Auth's password hasher

Email signups now bypass Better Auth but store a bcrypt hash in the credential account; the login path still uses authClient.signIn.email with the Better Auth server config, which has not been configured to verify bcrypt hashes and defaults to its own password format. After a user verifies email, password login for accounts created through /api/auth/custom-signup will reject the correct password because the stored hash format is incompatible.

Useful? React with 👍 / 👎.


// Check if already subscribed
const user = await prisma.user.findUnique({ where: { id: userId } })
if (user?.razorpaySubId && user?.planStatus === "active") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't treat pending Razorpay subscriptions as active

This guard blocks any user that has a razorpaySubId while planStatus is active, but FREE users also default to planStatus: "active", and createSubscription writes razorpaySubId before payment/webhook confirmation. If a checkout is dismissed or fails after the subscription is created, the next upgrade attempt will always hit this branch and the user cannot retry without manual cleanup.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (2)
src/lib/media-service.ts (1)

64-72: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The SSRF guard is still bypassable.

Line 69 only blocks a few hostname prefixes. It still allows private targets like 172.20.x.x, IPv6 loopback/ULA ranges, and hostnames that DNS-resolve to private IPs, so this server-side fetch can still hit internal services over HTTPS. Resolve the host and reject loopback/link-local/private CIDRs instead of relying on string matching.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/media-service.ts` around lines 64 - 72, The current string-prefix
check in MediaService (variables parsed, host, blocked) is insufficient; instead
perform a DNS resolution for parsed.hostname (follow CNAMEs / use dns.lookup or
dns.promises.lookup/all) and validate every resolved IP is public by checking
against IPv4 private/link-local ranges (10.0.0.0/8, 172.16.0.0/12,
169.254.0.0/16, 127.0.0.0/8, 0.0.0.0/8) and IPv6 loopback/ULA/link-local ranges
(::1/128, fc00::/7, fe80::/10), rejecting the request if any resolved address
falls in those CIDRs; ensure you handle both IPv4 and IPv6 addresses (use a
CIDR/IP utility or net.isIP + range checks) and throw the same MediaService
error when a private/internal IP is detected.
src/features/auth/components/login-form.tsx (1)

211-214: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the footer copy typo.

Don&apos;'t have an account? renders with an extra apostrophe in the login CTA.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/auth/components/login-form.tsx` around lines 211 - 214, The
footer text in the LoginForm component contains a typo "Don&apos;'t have an
account?" which renders an extra apostrophe; update the JSX string used in the
div with className "text-center text-sm" (the line containing "Don&apos;'t have
an account?") to the correct text "Don't have an account?" (or use the JSX-safe
variant without the extra entity) so the Link to "/signup" remains unchanged.
🟡 Minor comments (7)
src/components/structured-data.tsx-58-58 (1)

58-58: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update founding date to match current year.

The foundingDate is set to "2025", but the current date is June 2026. If the company was founded in 2025, this is correct. Otherwise, update it to the accurate founding year.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/structured-data.tsx` at line 58, Update the foundingDate
property in the structured data object (the "foundingDate" field in the JSON-LD
produced by the StructuredData/structuredData variable) to the correct year
(e.g., change "2025" to "2026" if the company was founded in 2026); ensure the
value is the accurate founding year as a string so the JSON-LD remains valid.
src/app/(marketing)/pricing/pricing-page.tsx-177-195 (1)

177-195: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit type="button" to the interval toggles.

These buttons currently rely on the browser default submit behavior. If this component is ever rendered inside a form, toggling billing interval will submit it.

Small fix
           <button
+            type="button"
             id="toggle-monthly"
             onClick={() => setInterval("monthly")}
@@
           <button
+            type="button"
             id="toggle-yearly"
             onClick={() => setInterval("yearly")}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(marketing)/pricing/pricing-page.tsx around lines 177 - 195, The
interval toggle buttons (ids "toggle-monthly" and "toggle-yearly") call
setInterval("monthly"/"yearly") but lack an explicit button type, which can
cause accidental form submissions; update both button elements used for toggling
(the ones calling setInterval) to include type="button" to prevent default
submit behavior when rendered inside a form.
src/features/executions/components/media-upload/dialog.tsx-167-167 (1)

167-167: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Contradictory wording: "permanent, 48-hour presigned URL".

A presigned URL with a 48-hour expiration is temporary by definition. The term "permanent" likely means the URL won't be revoked early, but this creates confusion. Line 290 has the same issue with "Permanent SAS URL (48 hrs)".

📝 Suggested fix for clearer wording
                     <DialogDescription>
-                        Upload media from any source to cloud storage and receive a permanent, 48-hour presigned URL.
+                        Upload media from any source to cloud storage and receive a presigned URL valid for 48 hours.
                     </DialogDescription>

And on line 290:

                             <ul className="list-disc pl-4 mt-2 space-y-1">
-                                <li><code>url</code> - Permanent SAS URL (48 hrs)</li>
+                                <li><code>url</code> - Presigned URL (valid 48 hours)</li>
                                 <li><code>mimeType</code> - e.g. &quot;image/png&quot;</li>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/executions/components/media-upload/dialog.tsx` at line 167, The
UI text is contradictory: replace the phrase "permanent, 48-hour presigned URL"
with clearer wording (e.g., "48‑hour presigned URL" or "persistent 48‑hour
presigned URL") and likewise change "Permanent SAS URL (48 hrs)" to "SAS URL (48
hrs)" or "Persistent SAS URL (48 hrs)"; update the two string literals shown in
the dialog component (the "Upload media from any source..." sentence and the
"Permanent SAS URL (48 hrs)" label) so they no longer use the word "permanent".
src/lib/media-service.ts-16-18 (1)

16-18: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate the signed-URL expiry before using it.

If DO_SPACES_SAS_EXPIRY_HOURS is empty, non-numeric, or non-positive, Line 157 produces an invalid expiresIn and this path fails after the upload has already succeeded, leaving orphaned objects behind. Clamp this to a sane positive integer and fall back to the default when parsing fails.

Suggested fix
-const SAS_EXPIRY_HOURS = parseInt(
-  process.env.DO_SPACES_SAS_EXPIRY_HOURS ?? "48"
-)
+const parsedExpiryHours = Number.parseInt(
+  process.env.DO_SPACES_SAS_EXPIRY_HOURS ?? "48",
+  10
+)
+const SAS_EXPIRY_HOURS =
+  Number.isFinite(parsedExpiryHours) && parsedExpiryHours > 0
+    ? parsedExpiryHours
+    : 48

Also applies to: 157-165

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/media-service.ts` around lines 16 - 18, SAS_EXPIRY_HOURS is parsed
directly from DO_SPACES_SAS_EXPIRY_HOURS and can be empty, non-numeric or
non-positive; change the parsing logic so that you parse the env var into a
number, validate it is a finite integer > 0, and if not fall back to the default
(48) and clamp to a sensible max if desired; update the constant
SAS_EXPIRY_HOURS and any code that uses it for signed URL creation (the
expiresIn parameter) to rely on this validated value so expiresIn is always a
positive integer.
src/app/api/auth/custom-signup/route.ts-12-14 (1)

12-14: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add email format and password strength validation.

The endpoint only checks for presence of email and password, but doesn't validate email format or enforce password requirements. Malformed emails may cause downstream issues; weak passwords reduce account security.

Proposed fix
+    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+    if (!emailRegex.test(email)) {
+      return NextResponse.json({ error: "Invalid email format" }, { status: 400 })
+    }
+
+    if (password.length < 8) {
+      return NextResponse.json({ error: "Password must be at least 8 characters" }, { status: 400 })
+    }
+
     // 1. Check if email already exists
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/auth/custom-signup/route.ts` around lines 12 - 14, The handler
currently only checks presence of email and password; add proper email format
validation and password strength checks before creating the user. In the route
handler (the POST request function that reads email and password and currently
does "if (!email || !password) ..."), validate email using a standard regex or
validator to ensure a well-formed address and enforce a password policy (e.g.,
minimum length, require numbers/letters and a special character or similar
rules), and return NextResponse.json({ error: "<specific message>" }, { status:
400 }) for invalid email or weak password. Keep these checks immediately after
extracting email/password and before any downstream processing or user creation
logic.
src/app/api/auth/resend-verification/route.ts-26-35 (1)

26-35: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Email send failure leaves attempts counter incremented.

If sendResendVerificationEmail throws (e.g., SMTP timeout), the emailVerifyAttempts counter has already been incremented. Legitimate users experiencing transient email failures would lose an attempt without receiving an email.

Proposed fix

Wrap the send in try/catch and only commit the update on success, or decrement on failure:

+    try {
+      await sendResendVerificationEmail(email, user.name || email, token)
+    } catch (emailError) {
+      // Rollback the attempt increment on email failure
+      await prisma.user.update({
+        where: { id: user.id },
+        data: { emailVerifyAttempts: { decrement: 1 } },
+      })
+      console.error("Failed to send verification email:", emailError)
+      return NextResponse.json(
+        { error: "Failed to send email. Please try again." },
+        { status: 500 }
+      )
+    }
-    await sendResendVerificationEmail(email, user.name || email, token)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/auth/resend-verification/route.ts` around lines 26 - 35, The
current flow calls prisma.user.update to increment emailVerifyAttempts before
calling sendResendVerificationEmail, so if sendResendVerificationEmail throws
the attempt counter is incorrectly consumed; change the flow so the database is
only updated when the email send succeeds (or on failure roll back the
increment). Concretely, generate the token as now, then call
sendResendVerificationEmail(email, user.name || email, token) inside a try
block; on success call prisma.user.update(...) to set emailVerifyToken,
emailVerifyExpiry (getTokenExpiry()) and increment emailVerifyAttempts; on
catch, do not modify the DB (or if you must update first, catch the error and
call prisma.user.update to decrement emailVerifyAttempts) so emailVerifyAttempts
accurately reflects only successful sends. Ensure you reference and update the
same fields: emailVerifyToken, emailVerifyExpiry, and emailVerifyAttempts.
src/app/(auth)/verify-email/page.tsx-15-46 (1)

15-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent duplicate verification POSTs for single-use tokens

The verify endpoint clears emailVerifyToken/emailVerifyExpiry after the first successful call, and subsequent calls with the same token return { error: "Invalid or already used verification link." }. If useEffect fires twice (e.g., React/Next dev Strict Mode effect replay), the second response can overwrite the UI into error/expired.

Suggested fix
-import { useEffect, useState, Suspense } from "react"
+import { useEffect, useRef, useState, Suspense } from "react"

 function VerifyEmailContent() {
   const searchParams = useSearchParams()
   const router = useRouter()
   const token = searchParams.get("token")
+  const hasSubmittedRef = useRef(false)

   const [status, setStatus] = useState<"loading" | "success" | "error" | "expired">("loading")
   const [message, setMessage] = useState("")

   useEffect(() => {
     if (!token) {
       setStatus("error")
       setMessage("No verification token found. Check your email for the correct link.")
       return
     }
+
+    if (hasSubmittedRef.current) return
+    hasSubmittedRef.current = true

-    // Call the verify API
     fetch("/api/auth/verify-email", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify({ token }),
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(auth)/verify-email/page.tsx around lines 15 - 46, The effect can
fire twice and re-post the same single-use token; modify the useEffect that
calls fetch("/api/auth/verify-email") to guard against duplicate POSTs by using
a persistent flag (e.g., a ref like hasVerifiedRef) or by checking current
status before sending, so once a successful verification sets
setStatus("success")/setMessage(...) further calls are ignored; update the
effect around the fetch call and its .then/.catch handlers (and any cleanup) to
check and set that flag (or early-return if status === "success") to prevent
overwriting the UI when the endpoint invalidates the token.
🧹 Nitpick comments (7)
src/app/layout.tsx (1)

72-79: ⚡ Quick win

Use absolute URL for OpenGraph image.

The OpenGraph images field uses a relative URL "./logos/logo.png". While Next.js should resolve this with metadataBase, absolute URLs are more reliable for social media crawlers.

🔧 Recommended fix
     images: [
       {
-        url: "./logos/logo.png",
+        url: "/logos/logo.png",
         width: 1200,
         height: 630,
         alt: "Nodebase — Workflow Automation Platform for India",
       },
     ],

Or use an absolute URL if the logo is hosted on a CDN.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/layout.tsx` around lines 72 - 79, The OpenGraph images entry in
metadata (the images array in src/app/layout.tsx) uses a relative path
("./logos/logo.png") which can fail for social crawlers; update the
images[0].url to an absolute URL (either by prepending the app's
metadataBase/origin or using a CDN absolute URL) so the url property is fully
qualified (e.g., https://example.com/logos/logo.png) while keeping the existing
width/height/alt fields unchanged.
src/app/api/webhooks/razorpay-billing/route.ts (1)

119-132: ⚡ Quick win

Period end calculation ignores Razorpay's actual billing cycle.

The handler calculates newPeriodEnd as now + 1 month instead of using the subscription's current_end from Razorpay. If payments are delayed or retried, this can cause the local period to drift from Razorpay's actual billing cycle. Consider extracting current_end from the subscription entity when available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/webhooks/razorpay-billing/route.ts` around lines 119 - 132, The
code sets newPeriodEnd to now+1 month (newPeriodEnd) before calling
prisma.user.update, which can drift from Razorpay's actual billing cycle;
instead, read the subscription entity's current_end (e.g.
subscription.current_end or payload.subscription.current_end) when present,
convert that epoch/ISO value to a Date, and use that Date as currentPeriodEnd in
the prisma.user.update call; keep the existing fallback logic to compute
newPeriodEnd if current_end is missing, and leave the other updates (planStatus,
workflowRunsUsed, workflowRunsReset) unchanged.
src/lib/execution-gate.ts (1)

27-40: ⚖️ Poor tradeoff

Monthly reset logic is duplicated with billing.ts:checkRunQuota.

Both checkExecutionLimit and checkRunQuota contain identical monthly reset logic. While both are idempotent, this duplication increases maintenance burden. Consider extracting the reset logic into a shared helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/execution-gate.ts` around lines 27 - 40, The monthly-reset block
duplicated in checkExecutionLimit (src/lib/execution-gate.ts) and
billing.ts:checkRunQuota should be extracted into a single helper (e.g.,
resetMonthlyWorkflowRuns or ensureMonthlyReset) that accepts prisma, userId, and
user.workflowRunsReset, performs the date comparison and prisma.user.update when
needed, and returns whether a reset occurred; replace the duplicated code in
both checkExecutionLimit and checkRunQuota with a call to this helper and remove
the original inline reset logic so both functions use the shared implementation.
.github/workflows/deploy-digitalocean.yml (3)

23-24: 💤 Low value

Consider adding persist-credentials: false to checkout steps for artifact security.

The actions/checkout steps do not explicitly disable credential persistence. If workflow artifacts or caches are compromised, persisted credentials could be extracted. Setting persist-credentials: false prevents this credential leakage vector.

🔒 Recommended configuration
       - name: Checkout repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false

Also applies to: 79-80

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-digitalocean.yml around lines 23 - 24, The checkout
steps using actions/checkout@v4 (the steps named "Checkout repository" and the
second checkout occurrence) should explicitly disable credential persistence:
add persist-credentials: false to each checkout step so the action does not
leave GitHub token credentials in the workspace or artifacts; locate the steps
that reference uses: actions/checkout@v4 and update their step definitions to
include persist-credentials: false.

24-24: ⚖️ Poor tradeoff

Consider pinning GitHub Actions to commit hashes for supply-chain security.

The workflow uses version tags (@v4, @v2) instead of commit SHA hashes. While version tags are more maintainable, they can be force-pushed or compromised. Pinning to immutable commit hashes provides stronger supply-chain guarantees.

🔐 Example: Pin actions/checkout@v4 to a hash
       - name: Checkout repository
-        uses: actions/checkout@v4
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

You can find commit hashes at: https://github.com/actions/checkout/releases

Also applies to: 27-27, 55-55, 80-80, 83-83

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-digitalocean.yml at line 24, Replace floating
version tags with immutable commit SHAs for all GitHub Actions used in this
workflow: locate the uses: entries for actions/checkout (currently `@v4`),
actions/setup-node ( `@v` ), digitalocean/action-doctl and any other uses at the
referenced lines, look up the corresponding release commit SHA on each action's
GitHub releases page, and update the uses: value from the tag (e.g., `@v4`) to the
exact commit SHA (e.g., @<full-commit-sha>) so each action is pinned to a
specific immutable commit.

47-68: ⚡ Quick win

Add explicit permissions block to deploy job for least-privilege security.

The deploy job inherits default GITHUB_TOKEN permissions, which may be overly broad. Explicitly declaring minimal required permissions improves security posture and makes the workflow's access requirements clear.

🛡️ Recommended permissions block
   deploy:
     name: Deploy to App Platform
     runs-on: ubuntu-latest
     needs: build-and-push
     if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+    permissions:
+      contents: read

     steps:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-digitalocean.yml around lines 47 - 68, The deploy
job currently relies on inherited GITHUB_TOKEN permissions; add an explicit
permissions block on the deploy job to enforce least-privilege. Edit the deploy
job (job name: deploy) and add a permissions map that restricts the GITHUB_TOKEN
to only what this workflow needs (e.g., set contents: read and disable other
scopes like actions, checks, and statuses or set them to none) so the doctl
steps and secret-based DigitalOcean deployment run without broad token rights.
Ensure the permissions block sits directly under the deploy job definition so it
overrides repository defaults.
src/lib/email-verification.ts (1)

113-199: 💤 Low value

Extract shared email template to reduce duplication.

sendResendVerificationEmail duplicates nearly the entire HTML/text template from sendVerificationEmail. Only the subject line and one sentence differ.

Suggested approach

Extract a shared helper:

function buildEmailContent(userName: string, verifyUrl: string, isResend: boolean) {
  const footerText = isResend
    ? "If you didn't request a new link, you can safely ignore this email."
    : "If you didn't create a Nodebase account, you can safely ignore this email."
  // ... return { html, text }
}

Then both send functions call buildEmailContent with the appropriate flag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/email-verification.ts` around lines 113 - 199,
sendResendVerificationEmail duplicates the full HTML/text template from
sendVerificationEmail; extract a shared helper (e.g.,
buildEmailContent(userName: string, verifyUrl: string, isResend: boolean)) that
returns { html, text } and use it in both sendVerificationEmail and
sendResendVerificationEmail, passing isResend to vary the subject/footer
sentence; update both functions to call buildEmailContent and only keep
differing subject lines and any small variations in the caller.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/deploy-digitalocean.yml:
- Around line 59-68: The current app discovery uses APP_ID=$(doctl apps list
--format ID --no-header | head -1) which can pick the wrong app; change the
workflow to use a concrete app identifier instead: add an APP_ID repository
secret and read that secret into APP_ID (use that value when calling doctl apps
create-deployment), and as a safe fallback replace the fragile doctl apps list +
head -1 with a filtered lookup by app name (use doctl apps list/describe to
match the known Nodebase app name and extract its ID) so that doctl apps
create-deployment "$APP_ID" targets the intended application.

In `@next-sitemap.config.js`:
- Around line 39-41: The additionalSitemaps array in next-sitemap.config.js is
adding "https://nodebase.tech/sitemap.xml" which duplicates the sitemap
next-sitemap already generates and causes a circular reference; remove that URL
from the additionalSitemaps array (or remove the additionalSitemaps property
entirely) so next-sitemap can manage sitemap entries and robots.txt
automatically, locating the configuration via the additionalSitemaps definition
in next-sitemap.config.js.

In `@public/manifest.json`:
- Around line 9-12: The manifest.json icons array references missing files via
icons[].src ("/favicon-192.png" and "/favicon-512.png"); either add the two PNG
files into the public/ directory with those exact names, or update the
icons[].src entries in public/manifest.json to point to existing image filenames
(e.g., existing favicon or logo assets) and ensure sizes/type fields remain
correct so the web app can load the declared icons.

In `@scripts/create-razorpay-plans.ts`:
- Around line 51-62: The script currently always creates new Razorpay plans in
createPlans by calling razorpay.plans.create for every entry and finishes with
createPlans().catch(console.error) which can hide failures and cause duplicates;
modify createPlans to first check for existing plan IDs (preferably: 1) if an
env var like RAZORPAY_PLAN_<TIER>_ID exists, skip creation and log reuse, and 2)
optionally query Razorpay for an existing plan matching plan.item.name/amount
and reuse that ID), only call razorpay.plans.create when no existing ID is
found, and on any error throw or process.exit(1) instead of silently logging via
.catch(console.error) so failures return a non‑zero exit code; update references
to razorpay.plans.create, createPlans, and RAZORPAY_PLAN_<TIER>_ID in the script
accordingly.

In `@src/app/`(marketing)/pricing/pricing-page.tsx:
- Around line 83-89: The Team plan entry (object with key "TEAM" in
pricing-page.tsx) currently uses cta: "Contact Sales" but ctaHref:
"/signup?plan=team", which deep-links to the self-serve signup instead of a
sales-led flow; update the Team plan object so the ctaHref points to the correct
sales/contact endpoint (or change the cta label to match the signup flow) by
editing the TEAM entry’s ctaHref field (and cta text if you prefer) to the
proper contact route used by the app (e.g., your sales contact route or a lead
form path) so the CTA consistently triggers a sales-led flow.
- Around line 25-102: The pricing amounts are duplicated in the plans array
(symbol: plans) causing a mismatch with the Razorpay plan creation script
(symbol: create-razorpay-plans.ts); fix by centralizing the canonical prices
into a shared export (e.g., PRICE_CATALOG or RAZORPAY_PLAN_CATALOG) and import
that into the component, then replace hard-coded monthlyPrice/yearlyPrice values
in the plans array with references to the shared catalog (keep using PLAN_LIMITS
for limits), and remove any inconsistent literals so the UI and billing script
consume the single source of truth.

In `@src/app/api/auth/custom-signup/route.ts`:
- Around line 24-31: The signup handler currently calls prisma.user.update and
resets emailVerifyAttempts to 0, enabling resend-rate-limit bypass; modify the
logic in the signup route (route.ts) so you do not reset emailVerifyAttempts
when updating an existing unverified user—only update emailVerifyToken and
emailVerifyExpiry (and leave emailVerifyAttempts unchanged), or alternatively
read the user first, enforce the resend limit by returning a 429 from the signup
flow if emailVerifyAttempts already meets the limit, and only then update the
token/expiry via prisma.user.update without touching emailVerifyAttempts.

In `@src/app/api/webhooks/razorpay-billing/route.ts`:
- Around line 5-21: The verifyWebhookSignature function can throw if the
incoming signature contains non-hex characters; update verifyWebhookSignature to
validate the signature is valid hex (and matching length) or wrap the
Buffer.from/crypto.timingSafeEqual calls in a try-catch so any decoding/compare
errors return false instead of throwing; specifically locate
verifyWebhookSignature and either add a hex-regex check for signature before
Buffer.from(signature, "hex") and return false on failure, or catch exceptions
around the Buffer.from/crypto.timingSafeEqual block and return false on error.

In `@src/components/structured-data.tsx`:
- Line 34: The screenshot URL in src/components/structured-data.tsx is pointing
to a non-existent asset ("https://nodebase.tech/og-image.png"); update the
"screenshot" field to point to the generated OG endpoint "/opengraph-image".
Also check src/app/layout.tsx and replace twitter.images: ["/og-image.png"] with
["/opengraph-image"] (or make both use the same "/opengraph-image" path) so both
StructuredData and layout reference the generated opengraph-image route.

In `@src/features/auth/components/register-form.tsx`:
- Around line 80-93: The onSubmit handler is currently leaking the user's email
into the redirect URL via router.push(`/check-email?email=...`); instead, stop
including PII in the query string by storing the email in ephemeral client state
(e.g., sessionStorage or a short-lived React context) before navigation and then
call router.push('/check-email') without query params; update the onSubmit
function (and any components that render the check-email screen) to read the
email from that ephemeral store (key like "pendingEmail") and remove the query
param usage in router.push.

In `@src/hooks/use-razorpay.ts`:
- Around line 45-47: Check for the presence of NEXT_PUBLIC_RAZORPAY_KEY_ID
before creating the options object in use-razorpay.ts; if the env var is missing
or falsy, throw or return early (e.g., in the hook initializer or in the
function that builds options) so you don't call Razorpay checkout with an
invalid config. Specifically, guard around the code that constructs options (the
object with key and subscription_id) and reference
process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID and subscriptionId to bail out
immediately with a clear error/return when the public key is undefined.

In `@src/inngest/functions/schedule-poller.ts`:
- Line 10: The cron schedule in schedule-poller.ts was changed to "{ cron: "0 9
* * *" }" which breaks the polling logic that checks "secondsSincePrev <= 60" to
decide firing; either revert the cron back to a frequent poll (e.g., "{ cron:
"*/1 * * * *" }" or every 5 minutes) so the existing secondsSincePrev check
continues to work, OR update the polling logic around the secondsSincePrev check
to detect and process missed runs (compute expected execution times since the
last run for each workflow and trigger each missed execution instead of only
firing when secondsSincePrev <= 60); locate the cron config and the
secondsSincePrev check in the polling function and implement one of these two
fixes.

In `@src/lib/email-verification.ts`:
- Around line 70-71: Add an HTML-escaping helper and use it wherever userName is
interpolated into the email HTML templates to prevent injection; implement a
function named escapeHtml(str: string) that replaces &, <, >, ", and ' with
their HTML entities, then replace direct uses of userName in the
template-building code (the HTML string blocks around the current userName
interpolation and the other occurrences noted at lines ~158-160) with
escapeHtml(userName) so all embedded names are safely escaped before being
inserted.

In `@src/lib/media-service.ts`:
- Around line 43-45: The UploadResult interface currently exposes url as a
presigned (expiring) Spaces URL which callers persist; change UploadResult to
return a stable identifier or durable public URL (e.g., blobName or a
non-expiring publicUrl field) instead of an expiring signed URL, and move
presigned URL creation into your read-time method (e.g.,
getMediaUrl/generatePresignedUrl or download handlers). Update any functions
that return UploadResult (such as uploadMedia/uploadToSpaces) to populate the
durable identifier/publicUrl field only, and ensure code paths referenced in the
later block (lines ~139-149) call the read-time presign function to obtain
expiring URLs when needed rather than storing them in UploadResult.
- Around line 242-251: The DeleteObjectsCommand response can include per-object
errors that don't throw, but the current code unconditionally increments deleted
by objects.length; change the logic around the client.send(new
DeleteObjectsCommand(...)) call to capture the response, inspect
response.Deleted and response.Errors (or equivalent fields), increment the
deleted counter only by the number of successful deletions returned in
response.Deleted, and if any entries appear in response.Errors either retry
those keys or propagate an error (e.g., throw or return a failure) so cleanup
does not report success while some keys remain; update any callers that rely on
the deleted count accordingly (refer to variables/functions: client.send,
DeleteObjectsCommand, objects, deleted, response.Deleted, response.Errors).

---

Outside diff comments:
In `@src/features/auth/components/login-form.tsx`:
- Around line 211-214: The footer text in the LoginForm component contains a
typo "Don&apos;'t have an account?" which renders an extra apostrophe; update
the JSX string used in the div with className "text-center text-sm" (the line
containing "Don&apos;'t have an account?") to the correct text "Don't have an
account?" (or use the JSX-safe variant without the extra entity) so the Link to
"/signup" remains unchanged.

In `@src/lib/media-service.ts`:
- Around line 64-72: The current string-prefix check in MediaService (variables
parsed, host, blocked) is insufficient; instead perform a DNS resolution for
parsed.hostname (follow CNAMEs / use dns.lookup or dns.promises.lookup/all) and
validate every resolved IP is public by checking against IPv4 private/link-local
ranges (10.0.0.0/8, 172.16.0.0/12, 169.254.0.0/16, 127.0.0.0/8, 0.0.0.0/8) and
IPv6 loopback/ULA/link-local ranges (::1/128, fc00::/7, fe80::/10), rejecting
the request if any resolved address falls in those CIDRs; ensure you handle both
IPv4 and IPv6 addresses (use a CIDR/IP utility or net.isIP + range checks) and
throw the same MediaService error when a private/internal IP is detected.

---

Minor comments:
In `@src/app/`(auth)/verify-email/page.tsx:
- Around line 15-46: The effect can fire twice and re-post the same single-use
token; modify the useEffect that calls fetch("/api/auth/verify-email") to guard
against duplicate POSTs by using a persistent flag (e.g., a ref like
hasVerifiedRef) or by checking current status before sending, so once a
successful verification sets setStatus("success")/setMessage(...) further calls
are ignored; update the effect around the fetch call and its .then/.catch
handlers (and any cleanup) to check and set that flag (or early-return if status
=== "success") to prevent overwriting the UI when the endpoint invalidates the
token.

In `@src/app/`(marketing)/pricing/pricing-page.tsx:
- Around line 177-195: The interval toggle buttons (ids "toggle-monthly" and
"toggle-yearly") call setInterval("monthly"/"yearly") but lack an explicit
button type, which can cause accidental form submissions; update both button
elements used for toggling (the ones calling setInterval) to include
type="button" to prevent default submit behavior when rendered inside a form.

In `@src/app/api/auth/custom-signup/route.ts`:
- Around line 12-14: The handler currently only checks presence of email and
password; add proper email format validation and password strength checks before
creating the user. In the route handler (the POST request function that reads
email and password and currently does "if (!email || !password) ..."), validate
email using a standard regex or validator to ensure a well-formed address and
enforce a password policy (e.g., minimum length, require numbers/letters and a
special character or similar rules), and return NextResponse.json({ error:
"<specific message>" }, { status: 400 }) for invalid email or weak password.
Keep these checks immediately after extracting email/password and before any
downstream processing or user creation logic.

In `@src/app/api/auth/resend-verification/route.ts`:
- Around line 26-35: The current flow calls prisma.user.update to increment
emailVerifyAttempts before calling sendResendVerificationEmail, so if
sendResendVerificationEmail throws the attempt counter is incorrectly consumed;
change the flow so the database is only updated when the email send succeeds (or
on failure roll back the increment). Concretely, generate the token as now, then
call sendResendVerificationEmail(email, user.name || email, token) inside a try
block; on success call prisma.user.update(...) to set emailVerifyToken,
emailVerifyExpiry (getTokenExpiry()) and increment emailVerifyAttempts; on
catch, do not modify the DB (or if you must update first, catch the error and
call prisma.user.update to decrement emailVerifyAttempts) so emailVerifyAttempts
accurately reflects only successful sends. Ensure you reference and update the
same fields: emailVerifyToken, emailVerifyExpiry, and emailVerifyAttempts.

In `@src/components/structured-data.tsx`:
- Line 58: Update the foundingDate property in the structured data object (the
"foundingDate" field in the JSON-LD produced by the
StructuredData/structuredData variable) to the correct year (e.g., change "2025"
to "2026" if the company was founded in 2026); ensure the value is the accurate
founding year as a string so the JSON-LD remains valid.

In `@src/features/executions/components/media-upload/dialog.tsx`:
- Line 167: The UI text is contradictory: replace the phrase "permanent, 48-hour
presigned URL" with clearer wording (e.g., "48‑hour presigned URL" or
"persistent 48‑hour presigned URL") and likewise change "Permanent SAS URL (48
hrs)" to "SAS URL (48 hrs)" or "Persistent SAS URL (48 hrs)"; update the two
string literals shown in the dialog component (the "Upload media from any
source..." sentence and the "Permanent SAS URL (48 hrs)" label) so they no
longer use the word "permanent".

In `@src/lib/media-service.ts`:
- Around line 16-18: SAS_EXPIRY_HOURS is parsed directly from
DO_SPACES_SAS_EXPIRY_HOURS and can be empty, non-numeric or non-positive; change
the parsing logic so that you parse the env var into a number, validate it is a
finite integer > 0, and if not fall back to the default (48) and clamp to a
sensible max if desired; update the constant SAS_EXPIRY_HOURS and any code that
uses it for signed URL creation (the expiresIn parameter) to rely on this
validated value so expiresIn is always a positive integer.

---

Nitpick comments:
In @.github/workflows/deploy-digitalocean.yml:
- Around line 23-24: The checkout steps using actions/checkout@v4 (the steps
named "Checkout repository" and the second checkout occurrence) should
explicitly disable credential persistence: add persist-credentials: false to
each checkout step so the action does not leave GitHub token credentials in the
workspace or artifacts; locate the steps that reference uses:
actions/checkout@v4 and update their step definitions to include
persist-credentials: false.
- Line 24: Replace floating version tags with immutable commit SHAs for all
GitHub Actions used in this workflow: locate the uses: entries for
actions/checkout (currently `@v4`), actions/setup-node ( `@v` ),
digitalocean/action-doctl and any other uses at the referenced lines, look up
the corresponding release commit SHA on each action's GitHub releases page, and
update the uses: value from the tag (e.g., `@v4`) to the exact commit SHA (e.g.,
@<full-commit-sha>) so each action is pinned to a specific immutable commit.
- Around line 47-68: The deploy job currently relies on inherited GITHUB_TOKEN
permissions; add an explicit permissions block on the deploy job to enforce
least-privilege. Edit the deploy job (job name: deploy) and add a permissions
map that restricts the GITHUB_TOKEN to only what this workflow needs (e.g., set
contents: read and disable other scopes like actions, checks, and statuses or
set them to none) so the doctl steps and secret-based DigitalOcean deployment
run without broad token rights. Ensure the permissions block sits directly under
the deploy job definition so it overrides repository defaults.

In `@src/app/api/webhooks/razorpay-billing/route.ts`:
- Around line 119-132: The code sets newPeriodEnd to now+1 month (newPeriodEnd)
before calling prisma.user.update, which can drift from Razorpay's actual
billing cycle; instead, read the subscription entity's current_end (e.g.
subscription.current_end or payload.subscription.current_end) when present,
convert that epoch/ISO value to a Date, and use that Date as currentPeriodEnd in
the prisma.user.update call; keep the existing fallback logic to compute
newPeriodEnd if current_end is missing, and leave the other updates (planStatus,
workflowRunsUsed, workflowRunsReset) unchanged.

In `@src/app/layout.tsx`:
- Around line 72-79: The OpenGraph images entry in metadata (the images array in
src/app/layout.tsx) uses a relative path ("./logos/logo.png") which can fail for
social crawlers; update the images[0].url to an absolute URL (either by
prepending the app's metadataBase/origin or using a CDN absolute URL) so the url
property is fully qualified (e.g., https://example.com/logos/logo.png) while
keeping the existing width/height/alt fields unchanged.

In `@src/lib/email-verification.ts`:
- Around line 113-199: sendResendVerificationEmail duplicates the full HTML/text
template from sendVerificationEmail; extract a shared helper (e.g.,
buildEmailContent(userName: string, verifyUrl: string, isResend: boolean)) that
returns { html, text } and use it in both sendVerificationEmail and
sendResendVerificationEmail, passing isResend to vary the subject/footer
sentence; update both functions to call buildEmailContent and only keep
differing subject lines and any small variations in the caller.

In `@src/lib/execution-gate.ts`:
- Around line 27-40: The monthly-reset block duplicated in checkExecutionLimit
(src/lib/execution-gate.ts) and billing.ts:checkRunQuota should be extracted
into a single helper (e.g., resetMonthlyWorkflowRuns or ensureMonthlyReset) that
accepts prisma, userId, and user.workflowRunsReset, performs the date comparison
and prisma.user.update when needed, and returns whether a reset occurred;
replace the duplicated code in both checkExecutionLimit and checkRunQuota with a
call to this helper and remove the original inline reset logic so both functions
use the shared implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd741fa0-3c9c-4344-85bb-33294fe0c125

📥 Commits

Reviewing files that changed from the base of the PR and between 4a3eb62 and c54a7bf.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (60)
  • .env.example
  • .github/workflows/deploy-digitalocean.yml
  • .github/workflows/deploy.yml
  • .github/workflows/nodebase-app-AutoDeployTrigger-5e61cb78-dd30-4542-9ea3-615a7ad15fd2.yml
  • Dockerfile
  • next-sitemap.config.js
  • next.config.ts
  • package.json
  • prisma/migrations/20260409154001_razorpay/migration.sql
  • prisma/migrations/20260409_add_razorpay_billing/migration.sql
  • prisma/schema.prisma
  • public/manifest.json
  • scripts/create-razorpay-plans.ts
  • src/app/(auth)/check-email/page.tsx
  • src/app/(auth)/resend-verification/page.tsx
  • src/app/(auth)/verify-email/page.tsx
  • src/app/(marketing)/pricing/page.tsx
  • src/app/(marketing)/pricing/pricing-page.tsx
  • src/app/api/auth/custom-signup/route.ts
  • src/app/api/auth/resend-verification/route.ts
  • src/app/api/auth/verify-email/route.ts
  • src/app/api/polar/webhook/route.ts
  • src/app/api/webhooks/razorpay-billing/route.ts
  • src/app/globals.css
  • src/app/layout.tsx
  • src/app/opengraph-image.tsx
  • src/components/app-sidebar.tsx
  • src/components/entity-components.tsx
  • src/components/landing/marketing-page.tsx
  • src/components/node-selector.tsx
  • src/components/structured-data.tsx
  • src/components/upgrade-modal.tsx
  • src/components/upgrade-prompt.tsx
  • src/components/usage-banner.tsx
  • src/features/auth/components/login-form.tsx
  • src/features/auth/components/register-form.tsx
  • src/features/auth/components/subscriptions/hooks/use-subscription.ts
  • src/features/executions/components/ai/executor.ts
  • src/features/executions/components/gmail/executor.ts
  • src/features/executions/components/media-upload/dialog.tsx
  • src/features/executions/components/whatsapp/executor.ts
  • src/features/triggers/components/google-form-trigger/executor.ts
  • src/features/triggers/components/manual-trigger/executor.ts
  • src/features/triggers/components/stripe-trigger/executor.ts
  • src/features/workflows/hooks/use-workflows.ts
  • src/hooks/use-razorpay.ts
  • src/inngest/functions/schedule-poller.ts
  • src/lib/auth-client.ts
  • src/lib/auth.ts
  • src/lib/billing.ts
  • src/lib/email-verification.ts
  • src/lib/execution-gate.ts
  • src/lib/media-service.ts
  • src/lib/plan-limits.ts
  • src/lib/polar.ts
  • src/lib/razorpay-billing.ts
  • src/server/routers/billing.router.ts
  • src/server/routers/usage.router.ts
  • src/trpc/init.ts
  • src/trpc/routers/_app.ts
💤 Files with no reviewable changes (8)
  • src/features/triggers/components/manual-trigger/executor.ts
  • src/lib/polar.ts
  • src/features/triggers/components/stripe-trigger/executor.ts
  • .github/workflows/nodebase-app-AutoDeployTrigger-5e61cb78-dd30-4542-9ea3-615a7ad15fd2.yml
  • src/app/api/polar/webhook/route.ts
  • src/server/routers/usage.router.ts
  • .github/workflows/deploy.yml
  • src/features/triggers/components/google-form-trigger/executor.ts

Comment thread .github/workflows/deploy-digitalocean.yml
Comment thread next-sitemap.config.js Outdated
Comment thread public/manifest.json
Comment thread scripts/create-razorpay-plans.ts Outdated
Comment thread src/app/(marketing)/pricing/pricing-page.tsx
Comment thread src/hooks/use-razorpay.ts
Comment thread src/inngest/functions/schedule-poller.ts Outdated
Comment thread src/lib/email-verification.ts Outdated
Comment thread src/lib/media-service.ts Outdated
Comment thread src/lib/media-service.ts Outdated
@Mayank-saraswal
Mayank-saraswal merged commit 572b186 into main Jun 3, 2026
3 of 4 checks passed
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