Overview

Short, copy-paste patterns that match aSaaSin conventions. Use yarn, keep server-only secrets off the client, and reuse shared UI (TransitionButton, ActionMessageToast).

Form submit (action + toast)

Submit a server action that returns ActionResponse, show success/error with ActionMessageToast, and keep UX consistent via TransitionButton.

// One-liner: client form that calls a typed action and shows toasts.
'use client'

import * as React from 'react'
import { TransitionButton } from '@/components/TransitionButton'
import { ActionMessageToast } from '@/components/ActionMessageToast'
import { updateUserProfileAction } from '@/actions/updateUserProfileAction'

export function ProfileForm() {
  const [isPending, startTransition] = React.useTransition()

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)

    startTransition(async () => {
      try {
        const res = await updateUserProfileAction(formData) // ActionResponse
        if (res?.success) ActionMessageToast.success('Profile updated')
        else ActionMessageToast.error(res?.error ?? 'Update failed')
      } catch {
        ActionMessageToast.error('Unexpected error')
      }
    })
  }

  return (
    <form onSubmit={onSubmit} className="grid gap-4">
      <input name="name" placeholder="Your name" />
      <input name="existingAvatar" type="hidden" value="" />
      <input name="avatarFile" type="file" accept="image/*" />
      <TransitionButton type="submit" isPending={isPending}>
        Save changes
      </TransitionButton>
    </form>
  )
}

Profile update + avatar

Accept FormData, upload a new file to Supabase Storage when provided, keep or clear the existing avatar accordingly, and update profiles.

// One-liner: server action that conditionally uploads avatar and updates Supabase.
import { createAdminClient } from '@/config/supabase/adminClient'
import { DatabaseTable } from '@/constants/enums'
import { getCurrentUser } from '@/lib/auth'
import { uploadAvatar } from '@/lib/storage'
import type { ActionResponse } from '@/types' // { success: boolean; error?: string; data?: T }

export async function updateUserProfileAction(formData: FormData): Promise<ActionResponse> {
  const supabase = createAdminClient()
  const user = await getCurrentUser()
  if (!user) return { success: false, error: 'Not authenticated' }

  const name = String(formData.get('name') ?? '')
  const file = formData.get('avatarFile') as File | null
  const existing = String(formData.get('existingAvatar') ?? '')
  const removed = existing === ''

  let avatarUrl: string | null = null
  if (file) avatarUrl = await uploadAvatar(file, user.id)
  else if (!removed) avatarUrl = existing || null

  const update: Record<string, string | null> = { full_name: name }
  if (file || removed) update.avatar_url = avatarUrl

  const { error } = await supabase
    .from(DatabaseTable.Profiles)
    .update(update)
    .eq('id', user.id)

  if (error) return { success: false, error: 'Failed to update profile' }
  return { success: true }
}

Keep redirect params

Forward redirect_to and variant_id so users land in the right place or continue checkout after auth.

// One-liner: keep redirect_to + variant_id when switching auth pages.
'use client'

import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { AppRoute } from '@/constants/routes'

export function SwitchAuthLink({ to }: { to: 'sign-in' | 'sign-up' }) {
  const sp = useSearchParams()
  const qp = new URLSearchParams()
  ;['redirect_to', 'variant_id'].forEach((k) => {
    const v = sp.get(k); if (v) qp.set(k, v)
  })
  const href =
    to === 'sign-in' ? `${AppRoute.SignInPage}?${qp}` : `${AppRoute.SignUpPage}?${qp}`

  return <Link href={href}>{to === 'sign-in' ? 'Sign in' : 'Create account'}</Link>
}

Post-auth redirect

Read redirect_to and optional variant_id on callback; prefer a safe internal path and hand off to checkout when present.

// One-liner: central post-auth redirect handler.
import { redirect } from 'next/navigation'
import { DashboardRoute } from '@/constants/routes'
import { startCheckoutAction } from '@/actions/startCheckoutAction' // returns { success, data: { url } }

export async function handlePostAuth(params: { redirect_to?: string | null; variant_id?: string | null }) {
  const { redirect_to, variant_id } = params

  if (variant_id) {
    const res = await startCheckoutAction({ variantId: variant_id })
    if (res?.success && res.data?.url) return redirect(res.data.url)
  }

  const safe = redirect_to?.startsWith('/') ? redirect_to : DashboardRoute.OverviewPage
  return redirect(safe)
}

There will be more recipes.