Skip to content

Harden the starter for production-minded use - #1

Draft
yishaik wants to merge 27 commits into
mainfrom
agent/production-hardening
Draft

Harden the starter for production-minded use#1
yishaik wants to merge 27 commits into
mainfrom
agent/production-hardening

Conversation

@yishaik

@yishaik yishaik commented Jul 12, 2026

Copy link
Copy Markdown
Owner

What changed

  • upgrades the runtime baseline to Node 20.9+, Next.js 16, React 19.2, current Supabase/Sentry/Resend major lines, ESLint 9, and Zod validation
  • migrates middleware.ts to the Next.js 16 proxy.ts convention
  • adds CI, CodeQL, Dependabot, repository regression tests, and a unified npm run check
  • adds typed environment validation while removing the unused Supabase service-role key from the default configuration
  • hardens direct-to-R2 uploads with authentication, MIME allowlisting, size limits, UUID keys, short-lived signatures, safe errors, and cancellation support
  • adds health/readiness endpoints and turns the dashboard into an integration diagnostics surface
  • adds a Supabase auth confirmation callback
  • introduces an idempotent migration with stronger profile RLS and upload metadata policies
  • adds security headers, an application error boundary, and SECURITY.md
  • rewrites the README to describe the actual free-tier/commercial boundary and production checklist

Why

The original repository was a strong reference implementation, but its $0 at scale framing and minimal demo flows could be interpreted as production-ready. This change keeps the core intentionally small while making its security boundaries, upgrade paths, operational checks, and provider limitations explicit.

Developer impact

  • Node 20.9+ is required.
  • Run npm install once to generate a fresh lockfile after the dependency-major upgrade.
  • Apply supabase/migrations/202607130001_harden_profiles_and_uploads.sql.
  • Existing public R2 URLs remain optional; private objects are recommended for user uploads.
  • Persistent rate limiting and anti-bot controls remain deployment-specific production checklist items rather than hard-wired vendor dependencies.

Validation

GitHub Actions runs lint, type checking, repository tests, a production build, dependency audit, and CodeQL. This PR is intentionally opened as a draft until those checks complete and any dependency migration issues are resolved.

@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
free-stack-starter Ready Ready Preview, Comment Jul 12, 2026 11:42pm

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request updates the starter template to Next.js 16 and React 19, introducing comprehensive security headers, environment variable validation with Zod, Sentry error handling, and a secure direct-to-R2 upload flow. It also adds health/readiness endpoints, database migrations, and repository tests. However, several critical issues were identified during the review: renaming middleware.ts to proxy.ts breaks Next.js middleware execution, the auth callback redirect validation is vulnerable to an Open Redirect attack, malformed JSON in the upload API returns a 500 error instead of a 400, and the upload component lacks cleanup on unmount. Additionally, HF_TOKEN should be integrated into the Zod environment schema for consistency.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread proxy.ts
Comment on lines +4 to 6
export async function proxy(request: NextRequest) {
return updateSession(request)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-critical critical

Next.js strictly requires the middleware file to be named middleware.ts (or middleware.js) and located in the root or src directory, with a default or named export of middleware.\n\nRenaming this file to proxy.ts and exporting proxy will cause Next.js to completely ignore it. As a result, the Supabase session update/refresh logic in updateSession(request) will never execute, causing user sessions to expire and fail to refresh.\n\nPlease rename this file back to middleware.ts and export middleware instead of proxy.

Suggested change
export async function proxy(request: NextRequest) {
return updateSession(request)
}
export async function middleware(request: NextRequest) {
return updateSession(request)
}

const url = new URL(request.url)
const code = url.searchParams.get('code')
const next = url.searchParams.get('next') || '/dashboard'
const safeNext = next.startsWith('/') && !next.startsWith('//') ? next : '/dashboard'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The current check next.startsWith('/') && !next.startsWith('//') is vulnerable to an Open Redirect attack.\n\nAn attacker can bypass this check by using a backslash after the initial slash, such as /\\example.com. In modern browsers and Node.js, new URL('/\\\\example.com', url.origin) resolves to http://example.com/ because the backslash is normalized to a forward slash.\n\nTo prevent this, ensure that next does not start with /\\ as well.

Suggested change
const safeNext = next.startsWith('/') && !next.startsWith('//') ? next : '/dashboard'
const safeNext = next.startsWith('/') && !next.startsWith('//') && !next.startsWith('/\\') ? next : '/dashboard'

Comment thread app/api/upload/route.ts
Comment on lines 12 to +14
try {
body = await req.json()
} catch {
return NextResponse.json({ error: 'invalid json' }, { status: 400 })
}
const parsed = uploadRequestSchema.safeParse(await req.json())
if (!parsed.success) return apiError('Invalid upload request', 400, parsed.error.flatten())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In the previous implementation, malformed JSON in the request body was caught and handled gracefully with a 400 Bad Request response.\n\nIn this updated version, any JSON parsing error from await req.json() will bubble up to the main catch block and trigger internalError(error). This returns a 500 Internal Server Error to the client and logs an exception to Sentry, which can lead to unnecessary noise and false alarms in your error monitoring for simple client-side errors.\n\nWe should separate the JSON parsing try-catch from the main try-catch to return a proper 400 status code for malformed client input.

  let json: unknown
  try {
    json = await req.json()
  } catch {
    return apiError('Invalid JSON', 400)
  }

  try {
    const parsed = uploadRequestSchema.safeParse(json)
    if (!parsed.success) return apiError('Invalid upload request', 400, parsed.error.flatten())

Comment thread components/UploadDemo.tsx
Comment on lines 14 to +17
export function UploadDemo() {
const [status, setStatus] = useState<string>('')
const [status, setStatus] = useState('')
const [url, setUrl] = useState<string | null>(null)
const abortRef = useRef<AbortController | null>(null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If the UploadDemo component unmounts while an upload is still in progress, the active fetch requests are not aborted, and attempting to update the state after unmounting can cause memory leaks or unexpected behavior.\n\nWe should add a useEffect hook to clean up and abort any active requests when the component unmounts.\n\nNote: Remember to import useEffect from 'react' at the top of the file.

export function UploadDemo() {
  const [status, setStatus] = useState('')
  const [url, setUrl] = useState<string | null>(null)
  const abortRef = useRef<AbortController | null>(null)

  useEffect(() => {
    return () => {
      abortRef.current?.abort()
    }
  }, [])

Comment thread app/dashboard/page.tsx
['Cloudflare R2', hasR2Env(), 'Private object storage and signed uploads'],
['Resend', Boolean(env.RESEND_API_KEY && env.EMAIL_FROM), 'Transactional email'],
['Sentry', Boolean(env.NEXT_PUBLIC_SENTRY_DSN), 'Errors and tracing'],
['Hugging Face', Boolean(process.env.HF_TOKEN), 'Optional AI access'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

For consistency and to fully leverage the typed environment validation, HF_TOKEN should be added to the Zod schema in lib/env.ts and accessed via env.HF_TOKEN instead of directly reading from process.env.

Suggested change
['Hugging Face', Boolean(process.env.HF_TOKEN), 'Optional AI access'],
['Hugging Face', Boolean(env.HF_TOKEN), 'Optional AI access'],

Comment thread lib/env.ts
Comment on lines +17 to +18
UPLOAD_MAX_BYTES: z.coerce.number().int().positive().default(10 * 1024 * 1024),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add HF_TOKEN to the validated environment schema so it can be accessed safely and consistently via env.HF_TOKEN across the application.

Suggested change
UPLOAD_MAX_BYTES: z.coerce.number().int().positive().default(10 * 1024 * 1024),
})
UPLOAD_MAX_BYTES: z.coerce.number().int().positive().default(10 * 1024 * 1024),
HF_TOKEN: z.string().optional(),
})

Comment thread tests/repository.test.mjs
Comment on lines +13 to +16
test('Next 16 proxy is present and legacy middleware is not referenced by docs', async () => {
assert.match(await text('proxy.ts'), /export async function proxy/)
assert.doesNotMatch(await text('README.md'), /middleware\.ts/)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since the middleware file must be named middleware.ts to be recognized by Next.js, this test should be updated to assert the presence of middleware.ts and the middleware function instead of proxy.ts.

test('Next middleware is present', async () => {
  assert.match(await text('middleware.ts'), /export async function middleware/)
})

@yishaik

yishaik commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

Review: not merging yet — two blocking issues

1. CI fails on its own configuration (root cause of the red validate check)

This PR deletes package-lock.json (asking the maintainer to regenerate it), but the new .github/workflows/ci.yml uses actions/setup-node@v4 with cache: npm, which hard-fails when no lockfile exists:

##[error]Dependencies lock file is not found in /home/runner/work/free-stack-starter/free-stack-starter. Supported file patterns: package-lock.json,npm-shrinkwrap.json,yarn.lock

Fix: commit the regenerated package-lock.json as part of this PR. A dependency-major upgrade (Next 16, React 19.2, ESLint 9, etc.) should ship its lockfile anyway so CI and contributors build a reproducible tree. (Alternatively drop cache: npm, but committing the lockfile is the right fix.)

2. The branch is based on a pre-rewrite main and would clobber the current product

This branch forked from 1ad7452, before main was rewritten into the Free Stack Directory (PR #2: 348-service catalog + API-key tester, commit 0894e65) and PR #3 (docs page + agent-access prompts, just merged). As written, this PR's README.md rewrite and landing/dashboard changes describe the old minimal starter and would overwrite the directory's README and pages.

Suggested rescope after rebasing onto current main:

  • Keep (still valuable): CI/CodeQL/Dependabot workflows, SECURITY.md, typed env validation (lib/env.ts), R2 upload hardening (auth, MIME allowlist, size limits, UUID keys, short-lived signatures), health/ready endpoints, Supabase auth callback + RLS migration, security headers, error boundary.
  • Rework: README.md and landing-page copy against the directory product, not the old starter.
  • Re-validate: the Next 16 upgrade and middleware.tsproxy.ts rename against the current pages (/, /test-keys, /docs).

Leaving as draft per the PR's own description ("draft until those checks complete"). Happy to re-review once it's rebased and CI is green.

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.

2 participants