Update Convex with no payload to be just like convex with payload but without payload

This commit is contained in:
Gabriel Brown committed 2026-06-21 15:35:42 -05:00
1 parent 13b8b36c4c
commit fba73a92ce
130 files changed
+15637 -32018

No files matched your search

+1 -1
View File
@@ -1 +1 @@
[{"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/forgot-password/layout.tsx":"1","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/forgot-password/page.tsx":"2","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/profile/layout.tsx":"3","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/profile/page.tsx":"4","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/sign-in/layout.tsx":"5","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/sign-in/page.tsx":"6","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/global-error.tsx":"7","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/layout.tsx":"8","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/page.tsx":"9","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/cta.tsx":"10","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/features.tsx":"11","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/hero.tsx":"12","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/index.tsx":"13","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/tech-stack.tsx":"14","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/gibs-auth.tsx":"15","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/index.tsx":"16","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/avatar-upload.tsx":"17","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/header.tsx":"18","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/index.tsx":"19","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/reset-password.tsx":"20","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/sign-out.tsx":"21","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/user-info.tsx":"22","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/footer/index.tsx":"23","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx":"24","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/index.tsx":"25","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/index.tsx":"26","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/ConvexClientProvider.tsx":"27","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/index.tsx":"28","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation-client.ts":"29","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation.ts":"30","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/metadata.ts":"31","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/proxy/ban-sus-ips.ts":"32","/home/gib/Documents/Code/convex-monorepo/apps/next/src/proxy.ts":"33"},{"size":298,"mtime":1768236967188,"results":"34","hashOfConfig":"35"},{"size":10607,"mtime":1768372347281,"results":"36","hashOfConfig":"35"},{"size":276,"mtime":1768236979857,"results":"37","hashOfConfig":"35"},{"size":1515,"mtime":1768372347312,"results":"38","hashOfConfig":"35"},{"size":274,"mtime":1768236849421,"results":"39","hashOfConfig":"35"},{"size":16567,"mtime":1774544486885,"results":"40","hashOfConfig":"35"},{"size":2410,"mtime":1774544486967,"results":"41","hashOfConfig":"35"},{"size":2042,"mtime":1774131581680,"results":"42","hashOfConfig":"35"},{"size":253,"mtime":1768372347406,"results":"43","hashOfConfig":"35"},{"size":1139,"mtime":1774544487103,"results":"44","hashOfConfig":"35"},{"size":2916,"mtime":1774544487137,"results":"45","hashOfConfig":"35"},{"size":4045,"mtime":1774544487187,"results":"46","hashOfConfig":"35"},{"size":141,"mtime":1768325738000,"results":"47","hashOfConfig":"35"},{"size":2697,"mtime":1774544487253,"results":"48","hashOfConfig":"35"},{"size":1081,"mtime":1768372347496,"results":"49","hashOfConfig":"35"},{"size":52,"mtime":1768188510036,"results":"50","hashOfConfig":"35"},{"size":6797,"mtime":1768372347532,"results":"51","hashOfConfig":"35"},{"size":384,"mtime":1774544487414,"results":"52","hashOfConfig":"35"},{"size":230,"mtime":1768238412936,"results":"53","hashOfConfig":"35"},{"size":5681,"mtime":1768372347569,"results":"54","hashOfConfig":"35"},{"size":1234,"mtime":1768372347581,"results":"55","hashOfConfig":"35"},{"size":4786,"mtime":1774544487590,"results":"56","hashOfConfig":"35"},{"size":4010,"mtime":1768372347611,"results":"57","hashOfConfig":"35"},{"size":2539,"mtime":1768372347626,"results":"58","hashOfConfig":"35"},{"size":499,"mtime":1768372347633,"results":"59","hashOfConfig":"35"},{"size":2470,"mtime":1774544487733,"results":"60","hashOfConfig":"35"},{"size":444,"mtime":1774544487759,"rLine truncated
[{"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/cta.tsx":"1","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/features.tsx":"2","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/hero.tsx":"3","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/index.tsx":"4","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/tech-stack.tsx":"5","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/gibs-auth.tsx":"6","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/index.tsx":"7","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/avatar-upload.tsx":"8","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/header.tsx":"9","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/index.tsx":"10","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/reset-password.tsx":"11","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/sign-out.tsx":"12","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/user-info.tsx":"13","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/footer/index.tsx":"14","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx":"15","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/index.tsx":"16","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/index.tsx":"17","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/ConvexClientProvider.tsx":"18","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/index.tsx":"19","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation-client.ts":"20","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation.ts":"21","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/metadata.ts":"22","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/proxy/ban-sus-ips.ts":"23","/home/gib/Documents/Code/convex-monorepo/apps/next/src/proxy.ts":"24","/home/gib/Documents/Code/convex-monorepo/apps/next/payload-types.ts":"25","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/forgot-password/layout.tsx":"26","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/forgot-password/page.tsx":"27","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/profile/layout.tsx":"28","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/profile/page.tsx":"29","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/sign-in/layout.tsx":"30","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/sign-in/page.tsx":"31","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/global-error.tsx":"32","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/layout.tsx":"33","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/page.tsx":"34","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/admin/[[...segments]]/not-found.tsx":"35","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/admin/[[...segments]]/page.tsx":"36","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/admin/importMap.js":"37","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/[...slug]/route.ts":"38","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/graphql/route.ts":"39","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/graphql-playground/route.ts":"40","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/layout.tsx":"41","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/content.ts":"42","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/faq.tsx":"43","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/logo-cloud.tsx":"44","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/page-builder.tsx":"45","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/pricing.tsx":"46","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/stats.tsx":"47","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/testimonials.tsx":"48","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/navigation.tsx":"49","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/payload/refresh-route-on-save.tsx":"50","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/payload/get-landing-page-content.ts":"51","/home/gib/Documents/Code/coLine truncated
+1 -1
View File
@@ -1 +1 @@
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56"],{"key":"57","value":"58"},{"key":"59","value":"60"},{"key":"61","value":"62"},{"key":"63","value":"64"},{"key":"65","value":"66"},{"key":"67","value":"68"},{"key":"69","value":"70"},{"key":"71","value":"72"},{"key":"73","value":"74"},{"key":"75","value":"76"},{"key":"77","value":"78"},{"key":"79","value":"80"},{"key":"81","value":"82"},{"key":"83","value":"84"},{"key":"85","value":"86"},{"key":"87","value":"88"},{"key":"89","value":"90"},{"key":"91","value":"92"},{"key":"93","value":"94"},{"key":"95","value":"96"},{"key":"97","value":"98"},{"key":"99","value":"100"},{"key":"101","value":"102"},{"key":"103","value":"104"},{"key":"105","value":"106"},{"key":"107","value":"108"},{"key":"109","value":"110"},{"key":"111","value":"112"},{"key":"113","value":"114"},{"key":"115","value":"116"},{"key":"117","value":"118"},{"key":"119","value":"120"},{"key":"121","value":"122"},{"key":"123","value":"124"},{"key":"125","value":"126"},{"key":"127","value":"128"},{"key":"129","value":"130"},{"key":"131","value":"132"},{"key":"133","value":"134"},{"key":"135","value":"136"},{"key":"137","value":"138"},{"key":"139","value":"140"},{"key":"141","value":"142"},{"key":"143","value":"144"},{"key":"145","value":"146"},{"key":"147","value":"148"},{"key":"149","value":"150"},{"key":"151","value":"152"},{"key":"153","value":"154"},{"key":"155","value":"156"},{"key":"157","value":"158"},{"key":"159","value":"160"},{"key":"161","value":"162"},{"key":"163","value":"164"},{"key":"165","value":"166"},{"key":"167","value":"168"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-black.svg",{"size":948,"mtime":1717458243000,"hash":"169"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/index.tsx",{"size":499,"mtime":1768372347633,"hash":"170","data":"171"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/gibs-auth.tsx",{"size":1081,"mtime":1768372347496,"hash":"172","data":"173"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-wordmark-black.svg",{"size":3110,"mtime":1717458243000,"hash":"174"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/gitea/gitea.svg",{"size":1844,"mtime":1747415494000,"hash":"175"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/sentry.server.config.ts",{"size":184,"mtime":1774544487955,"hash":"176","data":"177"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/reset-password.tsx",{"size":5681,"mtime":1768372347569,"hash":"178","data":"179"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-color.svg",{"size":948,"mtime":1717458243000,"hash":"180"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation-client.ts",{"size":982,"mtime":1774544487814,"hash":"181","data":"182"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/tech-stack.tsx",{"size":2697,"mtime":1774544487253,"hash":"183","data":"184"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/favicon-light.png",{"size":14406,"mtime":1717458300000,"hash":"185"},"/home/gib/Documents/Code/convex-monorepo/apps/next/.cache/tsbuildinfo.json",{"size":679713,"mtime":1774546246715,"hash":"186","data":"187"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/global-error.tsx",{"size":2410,"mtime":1774544486967,"hash":"188","data":"189"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/footer/index.tsx",{"size":4010,"mtime":1768372347611,"hash":"190","data":"191"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx",{"size":2539,"mtime":1768372347626,"hash":"192","data":"193"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/cta.tsx",{"size":1139,"mtime":1774544487103,"hash":"194","data":"195"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/sign-out.tsx",{"size":1234,"mtime":1768372347581,"hash":"196","data":"197"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/index.tsx",{"size":141,"mtime":1768325738000,"hash":"198","data":"199"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-white.svg",{"size":942,"mtime":1717458243000,"hash":"200"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/index.tsx",{"size":52,"mtime":1768188510036,"hash":"201","data":"202"},"/home/gib/Documents/Code/convex-monorepo/apps/next/postcss.config.js",{"size":63,"mtime":1766225879000,"hash":"203","data":"204"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convexLine truncated
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56","57","58","59","60","61","62","63","64","65","66","67","68","69","70","71","72","73","74","75","76","77","78","79","80","81","82","83","84","85","86","87","88","89","90","91","92","93","94","95","96","97","98","99","100","101","102","103","104","105","106","107","108","109","110","111","112","113","114","115","116","117","118","119","120","121","122","123","124","125","126","127","128","129","130","131","132","133","134","135","136","137","138","139","140","141","142","143","144","145","146","147"],{"key":"148","value":"149"},{"key":"150","value":"151"},{"key":"152","value":"153"},{"key":"154","value":"155"},{"key":"156","value":"157"},{"key":"158","value":"159"},{"key":"160","value":"161"},{"key":"162","value":"163"},{"key":"164","value":"165"},{"key":"166","value":"167"},{"key":"168","value":"169"},{"key":"170","value":"171"},{"key":"172","value":"173"},{"key":"174","value":"175"},{"key":"176","value":"177"},{"key":"178","value":"179"},{"key":"180","value":"181"},{"key":"182","value":"183"},{"key":"184","value":"185"},{"key":"186","value":"187"},{"key":"188","value":"189"},{"key":"190","value":"191"},{"key":"192","value":"193"},{"key":"194","value":"195"},{"key":"196","value":"197"},{"key":"198","value":"199"},{"key":"200","value":"201"},{"key":"202","value":"203"},{"key":"204","value":"205"},{"key":"206","value":"207"},{"key":"208","value":"209"},{"key":"210","value":"211"},{"key":"212","value":"213"},{"key":"214","value":"215"},{"key":"216","value":"217"},{"key":"218","value":"219"},{"key":"220","value":"221"},{"key":"222","value":"223"},{"key":"224","value":"225"},{"key":"226","value":"227"},{"key":"228","value":"229"},{"key":"230","value":"231"},{"key":"232","value":"233"},{"key":"234","value":"235"},{"key":"236","value":"237"},{"key":"238","value":"239"},{"key":"240","value":"241"},{"key":"242","value":"243"},{"key":"244","value":"245"},{"key":"246","value":"247"},{"key":"248","value":"249"},{"key":"250","value":"251"},{"key":"252","value":"253"},{"key":"254","value":"255"},{"key":"256","value":"257"},{"key":"258","value":"259"},{"key":"260","value":"261"},{"key":"262","value":"263"},{"key":"264","value":"265"},{"key":"266","value":"267"},{"key":"268","value":"269"},{"key":"270","value":"271"},{"key":"272","value":"273"},{"key":"274","value":"275"},{"key":"276","value":"277"},{"key":"278","value":"279"},{"key":"280","value":"281"},{"key":"282","value":"283"},{"key":"284","value":"285"},{"key":"286","value":"287"},{"key":"288","value":"289"},{"key":"290","value":"291"},{"key":"292","value":"293"},{"key":"294","value":"295"},{"key":"296","value":"297"},{"key":"298","value":"299"},{"key":"300","value":"301"},{"key":"302","value":"303"},{"key":"304","value":"305"},{"key":"306","value":"307"},{"key":"308","value":"309"},{"key":"310","value":"311"},{"key":"312","value":"313"},{"key":"314","value":"315"},{"key":"316","value":"317"},{"key":"318","value":"319"},{"key":"320","value":"321"},{"key":"322","value":"323"},{"key":"324","value":"325"},{"key":"326","value":"327"},{"key":"328","value":"329"},{"key":"330","value":"331"},{"key":"332","value":"333"},{"key":"334","value":"335"},{"key":"336","value":"337"},{"key":"338","value":"339"},{"key":"340","value":"341"},{"key":"342","value":"343"},{"key":"344","value":"345"},{"key":"346","value":"347"},{"key":"348","value":"349"},{"key":"350","value":"351"},{"key":"352","value":"353"},{"key":"354","value":"355"},{"key":"356","value":"357"},{"key":"358","value":"359"},{"key":"360","value":"361"},{"key":"362","value":"363"},{"key":"364","value":"365"},{"key":"366","value":"367"},{"key":"368","value":"369"},{"key":"370","value":"371"},{"key":"372","value":"373"},{"key":"374","value":"375"},{"key":"376","value":"377"},{"key":"378","value":"379"},{"key":"380","value":"381"},{"key":"382","value":"383"},{"key":"384","value":"385"},{"key":"386","value":"387"},{"key":"388","value":"389"},{"key":"390","value":"391"},{"key":"392","value":"393"},{"key":"394","value":"395"},{"key":"396","value":"397"},{"key":"398","value":"399"},{"key":"400","value":"401"},{"key":"402","value":"403"},{"key":"404","value":"405"},{"key":"406","value":"407"},{"key":"408","value":"409"},{"key":"410","value":"411"},{"key":"412","value":"413"},{"key":"414","value":"415"},{"key":"416","value":"417"},{"key":"418","value":"419"},{"key":"420","value":"421"},{"key":"422","value":"423"},{"key":"424","value":"425"},{"key":"426","value":"427"},{"key":"428","value":"429"},{"key":"430","value":"431"},{"key":"432","value":"433"},{"key":"434","value":"435"},{"key":"436","value":"437"},{"key":"438","value":"439"},{"key":"440","value":"441"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-black.svg",{"size":94Line truncated
File diff suppressed because it is too large. Load diff
+15 -6
View File
@@ -5,24 +5,28 @@
"private": true,
"scripts": {
"build": "bun with-env next build",
"build:env": "bun with-env next build",
"build:docker": "next build --webpack",
"clean": "git clean -xdf .cache .next .turbo node_modules",
"dev": "bun with-env next dev --turbo",
"dev:tunnel": "bun with-env next dev --turbo",
"dev:web": "bun with-env next dev --webpack",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint --flag unstable_native_nodejs_ts_config",
"start": "bun with-env next start",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
"test:unit": "NODE_ENV=test vitest run --project unit",
"test:integration": "NODE_ENV=test vitest run --project integration",
"test:component": "NODE_ENV=test vitest run --project component",
"with-env": "sh ../../scripts/with-env ${INFISICAL_ENV:-dev} --"
},
"dependencies": {
"@convex-dev/auth": "catalog:convex",
"@gib/backend": "workspace:*",
"@gib/ui": "workspace:*",
"@sentry/nextjs": "^10.43.0",
"@t3-oss/env-nextjs": "^0.13.10",
"@sentry/nextjs": "^10.46.0",
"@t3-oss/env-nextjs": "^0.13.11",
"convex": "catalog:convex",
"next": "^16.1.7",
"next": "^16.2.1",
"next-plausible": "^3.12.5",
"react": "catalog:react19",
"react-dom": "catalog:react19",
@@ -35,14 +39,19 @@
"@gib/prettier-config": "workspace:*",
"@gib/tailwind-config": "workspace:*",
"@gib/tsconfig": "workspace:*",
"@gib/vitest-config": "workspace:*",
"@testing-library/react": "catalog:test",
"@types/node": "catalog:",
"@types/react": "catalog:react19",
"@types/react-dom": "catalog:react19",
"baseline-browser-mapping": "^2.10.11",
"eslint": "catalog:",
"jsdom": "catalog:test",
"prettier": "catalog:",
"tailwindcss": "catalog:",
"tw-animate-css": "^1.4.0",
"typescript": "catalog:"
"typescript": "catalog:",
"vitest": "catalog:test"
},
"prettier": "@gib/prettier-config"
}
+12 -1
View File
@@ -1,14 +1,25 @@
import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { isAuthenticatedNextjs } from '@convex-dev/auth/nextjs/server';
export const generateMetadata = (): Metadata => {
return {
title: 'Profile',
robots: {
index: false,
follow: false,
googleBot: {
index: false,
follow: false,
},
},
};
};
const ProfileLayout = ({
const ProfileLayout = async ({
children,
}: Readonly<{ children: React.ReactNode }>) => {
if (!(await isAuthenticatedNextjs())) redirect('/sign-in');
return <>{children}</>;
};
export default ProfileLayout;
+2 -2
View File
@@ -159,7 +159,7 @@ const SignIn = () => {
};
const handleVerifyEmail = async (
values: z.infer<typeof verifyEmailFormSchema>,
_values: z.infer<typeof verifyEmailFormSchema>,
) => {
const formData = new FormData();
formData.append('code', code);
@@ -194,7 +194,7 @@ const SignIn = () => {
<FormField
control={verifyEmailForm.control}
name='code'
render={({ field }) => (
render={({ field: _field }) => (
<FormItem>
<FormLabel className='text-xl'>Code</FormLabel>
<FormControl>
+10 -10
View File
@@ -1,12 +1,12 @@
import { CTA, Features, Hero, TechStack } from '@/components/landing';
export default function Home() {
return (
<main className='flex min-h-screen flex-col'>
<Hero />
<Features />
<TechStack />
<CTA />
</main>
);
}
const Home = () => (
<main className='flex min-h-screen flex-col'>
<Hero />
<Features />
<TechStack />
<CTA />
</main>
);
export default Home;
+5 -1
View File
@@ -2,7 +2,7 @@
@import 'tw-animate-css';
@import '@gib/tailwind-config/theme';
@source '../../../../packages/ui/src/*.{ts,tsx}';
@source '../../../../../packages/ui/src/*.{ts,tsx}';
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant light (&:where(.light, .light *));
@@ -23,7 +23,11 @@
* {
@apply border-border;
}
html {
@apply bg-background;
}
body {
@apply bg-background text-foreground;
letter-spacing: var(--tracking-normal);
}
}
-1
View File
@@ -10,7 +10,6 @@ export const CTA = () => (
everything pre-configured.
</p>
{/* Quick Start Command */}
<div className='mt-12'>
<p className='text-muted-foreground mb-3 text-sm font-medium'>
Quick Start
@@ -60,7 +60,6 @@ const features = [
export const Features = () => (
<section id='features' className='container mx-auto px-4 py-24'>
<div className='mx-auto max-w-6xl'>
{/* Section Header */}
<div className='mb-16 text-center'>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
Everything You Need to Ship Fast
@@ -71,7 +70,6 @@ export const Features = () => (
</p>
</div>
{/* Features Grid */}
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
{features.map((feature) => (
<Card key={feature.title} className='border-border/40'>
+20 -69
View File
@@ -9,16 +9,16 @@ const kanitSans = Kanit({
weight: ['400', '500', '600', '700'],
});
const highlights = ['TypeScript', 'Self-Hosted', 'Real-time', 'Auth Included'];
export const Hero = () => (
<section className='container mx-auto px-4 py-24 md:py-32 lg:py-40'>
<div className='mx-auto flex max-w-5xl flex-col items-center gap-8 text-center'>
{/* Badge */}
<div className='border-border/40 bg-muted/50 inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium'>
<span className='mr-2'>🚀</span>
<span>Production-ready monorepo template</span>
</div>
{/* Heading */}
<h1 className='from-foreground to-foreground/70 bg-linear-to-br bg-clip-text text-4xl font-bold tracking-tight text-transparent sm:text-5xl md:text-6xl lg:text-7xl'>
Build Full-Stack Apps with{' '}
<span
@@ -28,14 +28,12 @@ export const Hero = () => (
</span>
</h1>
{/* Description */}
<p className='text-muted-foreground max-w-2xl text-lg md:text-xl'>
A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship web
and mobile apps faster with shared code, type-safe backend, and complete
control over your infrastructure.
</p>
{/* CTA Buttons */}
<div className='flex flex-col gap-3 sm:flex-row'>
<Button size='lg' variant='outline' asChild>
<Link
@@ -54,72 +52,25 @@ export const Hero = () => (
</Button>
</div>
{/* Features Quick List */}
<div className='text-muted-foreground mt-8 flex flex-wrap items-center justify-center gap-6 text-sm'>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>TypeScript</span>
</div>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>Self-Hosted</span>
</div>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>Real-time</span>
</div>
<div className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>Auth Included</span>
</div>
{highlights.map((highlight) => (
<div key={highlight} className='flex items-center gap-2'>
<svg
className='h-5 w-5 text-green-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M5 13l4 4L19 7'
/>
</svg>
<span>{highlight}</span>
</div>
))}
</div>
</div>
</section>
@@ -29,7 +29,7 @@ const techStack = [
technologies: [
{ name: 'Turborepo', description: 'High-performance build system' },
{ name: 'TypeScript', description: 'Type-safe development' },
{ name: 'Bun', description: 'Fast package manager & runtime' },
{ name: 'Bun', description: 'Fast package manager and runtime' },
{ name: 'ESLint + Prettier', description: 'Code quality tools' },
{ name: 'Docker', description: 'Containerized deployment' },
],
@@ -40,7 +40,6 @@ export const TechStack = () => (
<section id='tech-stack' className='border-border/40 bg-muted/30 border-t'>
<div className='container mx-auto px-4 py-24'>
<div className='mx-auto max-w-6xl'>
{/* Section Header */}
<div className='mb-16 text-center'>
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
Modern Tech Stack
@@ -51,7 +50,6 @@ export const TechStack = () => (
</p>
</div>
{/* Tech Stack Grid */}
<div className='grid gap-12 md:grid-cols-3'>
{techStack.map((stack) => (
<div key={stack.category}>
@@ -21,9 +21,9 @@ import {
Input,
} from '@gib/ui';
interface AvatarUploadProps {
type AvatarUploadProps = {
preloadedUser: Preloaded<typeof api.auth.getUser>;
}
};
const dataUrlToBlob = async (
dataUrl: string,
@@ -1,17 +1,54 @@
'use client';
import type { ComponentProps } from 'react';
import { Kanit } from 'next/font/google';
import Image from 'next/image';
import Link from 'next/link';
import { Coffee, Server, Wrench } from 'lucide-react';
import { useConvexAuth, useQuery } from 'convex/react';
import { Coffee, Server, User, Wrench } from 'lucide-react';
import { api } from '@gib/backend/convex/_generated/api.js';
import type { NavItem } from './navigation';
import { Controls } from './controls';
import { DesktopNavigation, MobileNavigation } from './navigation';
const kanitSans = Kanit({
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
});
export default function Header(headerProps: ComponentProps<'header'>) {
const Header = (headerProps: ComponentProps<'header'>) => {
const { isAuthenticated } = useConvexAuth();
const user = useQuery(api.auth.getUser, isAuthenticated ? {} : 'skip');
const navItems: NavItem[] = [
{
href: '/#features',
icon: Wrench,
label: 'Features',
},
{
href: '/#tech-stack',
icon: Server,
label: 'Stack',
},
{
href: 'https://git.gbrown.org/gib/convex-monorepo',
icon: Coffee,
label: 'Repository',
external: true,
},
];
if (user?.isAdmin) {
navItems.push({
href: '/admin',
icon: User,
label: 'Admin',
external: true,
});
}
return (
<header
className='border-border/40 bg-background/95 supports-backdrop-filter:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur'
@@ -28,45 +65,24 @@ export default function Header(headerProps: ComponentProps<'header'>) {
alt='Convex Monorepo'
width={50}
height={50}
className='w-15 invert dark:invert-0'
className='w-10 invert lg:w-15 dark:invert-0'
/>
<span
className={`mb-3 hidden font-extrabold lg:inline lg:text-5xl ${kanitSans.className}`}
className={`hidden font-extrabold sm:mb-1 sm:text-lg md:inline lg:mb-3 lg:text-4xl xl:text-5xl ${kanitSans.className}`}
>
convex monorepo
convex-monorepo
</span>
</Link>
{/* Navigation */}
<nav className='hidden items-center gap-6 text-base font-medium md:flex'>
<Link
href='/#features'
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
>
<Wrench width={18} height={18} />
Features
</Link>
<Link
href='/#tech-stack'
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
>
<Server width={18} height={18} />
Stack
</Link>
<Link
href='https://git.gbrown.org/gib/convex-monorepo'
target='_blank'
rel='noopener noreferrer'
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
>
<Coffee width={20} height={20} />
Repository
</Link>
</nav>
<DesktopNavigation items={navItems} />
{/* Controls (Theme + Auth) */}
<Controls />
<div className='flex items-center gap-2'>
<Controls />
<MobileNavigation items={navItems} />
</div>
</div>
</header>
);
}
};
export default Header;
@@ -0,0 +1,97 @@
'use client';
import type { LucideIcon } from 'lucide-react';
import Link from 'next/link';
import { ExternalLink, Menu } from 'lucide-react';
import {
Button,
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@gib/ui';
export type NavItem = {
href: string;
icon: LucideIcon;
label: string;
external?: boolean;
};
type NavigationProps = {
items: NavItem[];
};
const DesktopNavigation = ({ items }: NavigationProps) => {
return (
<nav className='hidden items-center gap-4 text-xs font-medium sm:flex md:gap-6 lg:text-base'>
{items.map(({ href, icon: Icon, label, external }) => (
<Link
key={label}
href={href}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
>
<Icon width={18} height={18} />
{label}
</Link>
))}
</nav>
);
};
const MobileNavigation = ({ items }: NavigationProps) => {
return (
<Sheet>
<SheetTrigger asChild>
<Button
variant='outline'
size='icon-sm'
className='sm:hidden'
aria-label='Open navigation menu'
>
<Menu className='size-4.5' />
<span className='sr-only'>Open navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side='right' className='w-[min(88vw,22rem)] px-0'>
<SheetHeader className='border-border/60 from-background to-muted/40 border-b bg-linear-to-br from-35% px-5 py-5 text-left'>
<SheetTitle className='text-left text-lg'>Navigation</SheetTitle>
<SheetDescription className='text-left'>
Quick access to the links that collapse out of the header.
</SheetDescription>
</SheetHeader>
<div className='flex flex-col gap-3 px-4 py-5'>
{items.map(({ href, icon: Icon, label, external }) => (
<SheetClose asChild key={label}>
<Link
href={href}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
className='bg-card hover:bg-muted/70 border-border/60 text-card-foreground flex items-center justify-between rounded-2xl border px-4 py-3 transition-colors'
>
<span className='flex items-center gap-3'>
<span className='bg-muted text-foreground flex h-9 w-9 items-center justify-center rounded-xl'>
<Icon className='size-4.5' />
</span>
<span className='text-sm font-medium'>{label}</span>
</span>
{external ? (
<ExternalLink className='text-muted-foreground size-4' />
) : null}
</Link>
</SheetClose>
))}
</div>
</SheetContent>
</Sheet>
);
};
export { DesktopNavigation, MobileNavigation };
+1 -1
View File
@@ -19,7 +19,7 @@ Sentry.init({
tracesSampleRate: 1,
enableLogs: true,
// https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration
replaysSessionSampleRate: 1.0,
replaysSessionSampleRate: 0.5,
replaysOnErrorSampleRate: 1.0,
debug: false,
});
+38 -14
View File
@@ -4,7 +4,8 @@ import { NextResponse } from 'next/server';
// In-memory stores for tracking IPs (use Redis in production)
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
const bannedIPs = new Set<string>();
// Map of ip -> ban expiry timestamp. Avoids setTimeout closures leaking on hot reload.
const bannedIPs = new Map<string, number>();
// Suspicious patterns that indicate malicious activity
const MALICIOUS_PATTERNS = [
@@ -72,6 +73,36 @@ const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes
const MAX_404_ATTEMPTS = 10; // Max 404s before ban
let lastCleanup = Date.now();
const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
// Lazily purge stale entries so Maps don't grow without bound.
// Called on every request but only iterates Maps every CLEANUP_INTERVAL.
const cleanupStaleMaps = () => {
const now = Date.now();
if (now - lastCleanup < CLEANUP_INTERVAL) return;
lastCleanup = now;
for (const [ip, data] of ipAttempts.entries()) {
if (now - data.lastAttempt > RATE_LIMIT_WINDOW) ipAttempts.delete(ip);
}
for (const [ip, data] of ip404Attempts.entries()) {
if (now - data.lastAttempt > RATE_404_WINDOW) ip404Attempts.delete(ip);
}
for (const [ip, expiry] of bannedIPs.entries()) {
if (now > expiry) bannedIPs.delete(ip);
}
};
const isIPBanned = (ip: string): boolean => {
const expiry = bannedIPs.get(ip);
if (expiry === undefined) return false;
if (Date.now() > expiry) {
bannedIPs.delete(ip);
return false;
}
return true;
};
const getClientIP = (request: NextRequest): string => {
const forwarded = request.headers.get('x-forwarded-for');
const realIP = request.headers.get('x-real-ip');
@@ -104,13 +135,8 @@ const updateIPAttempts = (ip: string): boolean => {
attempts.lastAttempt = now;
if (attempts.count > MAX_ATTEMPTS) {
bannedIPs.add(ip);
bannedIPs.set(ip, Date.now() + BAN_DURATION);
ipAttempts.delete(ip);
setTimeout(() => {
bannedIPs.delete(ip);
}, BAN_DURATION);
return true;
}
@@ -130,17 +156,13 @@ const update404Attempts = (ip: string): boolean => {
attempts.lastAttempt = now;
if (attempts.count > MAX_404_ATTEMPTS) {
bannedIPs.add(ip);
bannedIPs.set(ip, Date.now() + BAN_DURATION);
ip404Attempts.delete(ip);
console.log(
`🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`,
);
setTimeout(() => {
bannedIPs.delete(ip);
}, BAN_DURATION);
return true;
}
@@ -148,12 +170,14 @@ const update404Attempts = (ip: string): boolean => {
};
export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
cleanupStaleMaps();
const { pathname } = request.nextUrl;
const method = request.method;
const ip = getClientIP(request);
// Check if IP is already banned
if (bannedIPs.has(ip)) {
if (isIPBanned(ip)) {
return new NextResponse('Access denied.', { status: 403 });
}
@@ -183,7 +207,7 @@ export const handle404Response = (
): NextResponse | null => {
const ip = getClientIP(request);
if (bannedIPs.has(ip)) {
if (isIPBanned(ip)) {
return new NextResponse('Access denied.', { status: 403 });
}
+21 -5
View File
@@ -1,19 +1,36 @@
import { convexAuthNextjsMiddleware } from '@convex-dev/auth/nextjs/server';
export default convexAuthNextjsMiddleware();
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
'/((?!.*\\..*|_next).*)',
'/',
'/(api)(.*)',
],
};
/**
import { banSuspiciousIPs } from '@/lib/proxy/ban-sus-ips';
import {
convexAuthNextjsMiddleware,
createRouteMatcher,
nextjsMiddlewareRedirect,
} from '@convex-dev/auth/nextjs/server';
const isSignInPage = createRouteMatcher(['/sign-in']);
const isProtectedRoute = createRouteMatcher(['/profile']);
const isProtectedRoute = createRouteMatcher([
'/profile(.*)',
'/dashboard(.*)',
'/admin-panel(.*)',
]);
export default convexAuthNextjsMiddleware(
async (request, { convexAuth }) => {
const banResponse = banSuspiciousIPs(request);
if (banResponse) return banResponse;
if (isSignInPage(request) && (await convexAuth.isAuthenticated())) {
return nextjsMiddlewareRedirect(request, '/');
return nextjsMiddlewareRedirect(request, '/profile');
}
if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) {
return nextjsMiddlewareRedirect(request, '/sign-in');
@@ -23,8 +40,6 @@ export default convexAuthNextjsMiddleware(
);
export const config = {
// The following matcher runs middleware on all routes
// except static assets.
matcher: [
'/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
'/((?!.*\\..*|_next).*)',
@@ -32,3 +47,4 @@ export const config = {
'/(api)(.*)',
],
};
**/
+11
View File
@@ -0,0 +1,11 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
const Greeting = ({ name }: { name: string }) => <p>Hello {name}</p>;
describe('component test harness', () => {
it('renders through jsdom and Testing Library', () => {
render(<Greeting name='world' />);
expect(screen.getByText('Hello world')).toBeInTheDocument();
});
});
@@ -0,0 +1,7 @@
import { describe, expect, it } from 'vitest';
describe('integration test harness', () => {
it('provides modern Node globals', () => {
expect(typeof globalThis.structuredClone).toBe('function');
});
});
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';
+7
View File
@@ -0,0 +1,7 @@
import { describe, expect, it } from 'vitest';
describe('unit test harness', () => {
it('executes isolated assertions', () => {
expect(1 + 1).toBe(2);
});
});
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config';
import { jsdomProject, nodeProject } from '@gib/vitest-config';
export default defineConfig({
test: {
projects: [
nodeProject('unit', ['tests/unit/**/*.test.{ts,tsx}']),
nodeProject('integration', ['tests/integration/**/*.test.{ts,tsx}']),
jsdomProject('component', ['tests/component/**/*.test.{ts,tsx}']),
],
},
});