@girardmedia/bootspring 3.3.2 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/assets/agents/accessibility-auditor.md +39 -0
  2. package/assets/agents/api-designer.md +40 -0
  3. package/assets/agents/auth-implementer.md +64 -0
  4. package/assets/agents/bug-hunter.md +42 -0
  5. package/assets/agents/bundle-analyzer.md +40 -0
  6. package/assets/agents/cache-optimizer.md +55 -0
  7. package/assets/agents/changelog-writer.md +55 -0
  8. package/assets/agents/ci-cd-builder.md +40 -0
  9. package/assets/agents/code-explainer.md +39 -0
  10. package/assets/agents/code-reviewer.md +39 -0
  11. package/assets/agents/cost-optimizer.md +57 -0
  12. package/assets/agents/cron-scheduler.md +51 -0
  13. package/assets/agents/data-seeder.md +56 -0
  14. package/assets/agents/database-architect.md +40 -0
  15. package/assets/agents/dependency-updater.md +40 -0
  16. package/assets/agents/deploy-checker.md +40 -0
  17. package/assets/agents/docker-optimizer.md +40 -0
  18. package/assets/agents/documentation-writer.md +40 -0
  19. package/assets/agents/email-builder.md +55 -0
  20. package/assets/agents/env-setup.md +40 -0
  21. package/assets/agents/error-handler.md +40 -0
  22. package/assets/agents/eslint-fixer.md +46 -0
  23. package/assets/agents/feature-flagger.md +69 -0
  24. package/assets/agents/git-detective.md +39 -0
  25. package/assets/agents/graphql-builder.md +60 -0
  26. package/assets/agents/incident-responder.md +59 -0
  27. package/assets/agents/log-analyzer.md +39 -0
  28. package/assets/agents/migration-planner.md +41 -0
  29. package/assets/agents/monorepo-navigator.md +39 -0
  30. package/assets/agents/nextjs-expert.md +57 -0
  31. package/assets/agents/notification-builder.md +56 -0
  32. package/assets/agents/onboarding-guide.md +39 -0
  33. package/assets/agents/performance-profiler.md +40 -0
  34. package/assets/agents/prisma-expert.md +57 -0
  35. package/assets/agents/rate-limiter.md +58 -0
  36. package/assets/agents/react-expert.md +58 -0
  37. package/assets/agents/refactorer.md +42 -0
  38. package/assets/agents/regex-builder.md +46 -0
  39. package/assets/agents/release-manager.md +40 -0
  40. package/assets/agents/s3-manager.md +58 -0
  41. package/assets/agents/schema-validator.md +40 -0
  42. package/assets/agents/search-builder.md +62 -0
  43. package/assets/agents/security-auditor.md +39 -0
  44. package/assets/agents/sitemap-generator.md +53 -0
  45. package/assets/agents/stripe-integrator.md +59 -0
  46. package/assets/agents/tailwind-expert.md +55 -0
  47. package/assets/agents/tech-debt-tracker.md +39 -0
  48. package/assets/agents/test-writer.md +42 -0
  49. package/assets/agents/type-fixer.md +45 -0
  50. package/assets/agents/webhook-builder.md +54 -0
  51. package/assets/rules/cpp.md +53 -0
  52. package/assets/rules/css.md +52 -0
  53. package/assets/rules/go.md +50 -0
  54. package/assets/rules/html.md +52 -0
  55. package/assets/rules/java.md +51 -0
  56. package/assets/rules/kotlin.md +50 -0
  57. package/assets/rules/php.md +51 -0
  58. package/assets/rules/python.md +51 -0
  59. package/assets/rules/ruby.md +51 -0
  60. package/assets/rules/rust.md +49 -0
  61. package/assets/rules/shell.md +52 -0
  62. package/assets/rules/sql.md +49 -0
  63. package/assets/rules/swift.md +50 -0
  64. package/assets/rules/typescript.md +52 -0
  65. package/assets/rules/yaml-json.md +51 -0
  66. package/assets/skills/accessibility.md +210 -0
  67. package/assets/skills/agent-patterns.md +387 -0
  68. package/assets/skills/ai-integration.md +263 -0
  69. package/assets/skills/animation-patterns.md +224 -0
  70. package/assets/skills/api-design.md +218 -0
  71. package/assets/skills/api-gateway.md +341 -0
  72. package/assets/skills/api-versioning.md +226 -0
  73. package/assets/skills/astro-patterns.md +233 -0
  74. package/assets/skills/auth-patterns.md +248 -0
  75. package/assets/skills/aws-patterns.md +171 -0
  76. package/assets/skills/background-jobs.md +162 -0
  77. package/assets/skills/browser-extensions.md +309 -0
  78. package/assets/skills/caching-patterns.md +253 -0
  79. package/assets/skills/ci-cd.md +251 -0
  80. package/assets/skills/cli-development.md +296 -0
  81. package/assets/skills/code-review.md +185 -0
  82. package/assets/skills/cron-patterns.md +327 -0
  83. package/assets/skills/data-fetching.md +231 -0
  84. package/assets/skills/database-migrations.md +346 -0
  85. package/assets/skills/database-patterns.md +219 -0
  86. package/assets/skills/debugging.md +281 -0
  87. package/assets/skills/design-system.md +289 -0
  88. package/assets/skills/django-patterns.md +182 -0
  89. package/assets/skills/docker-patterns.md +235 -0
  90. package/assets/skills/e2e-testing.md +287 -0
  91. package/assets/skills/edge-computing.md +268 -0
  92. package/assets/skills/electron-patterns.md +266 -0
  93. package/assets/skills/email-templates.md +206 -0
  94. package/assets/skills/error-handling.md +265 -0
  95. package/assets/skills/event-driven.md +232 -0
  96. package/assets/skills/express-patterns.md +239 -0
  97. package/assets/skills/fastapi-patterns.md +198 -0
  98. package/assets/skills/feature-flags.md +212 -0
  99. package/assets/skills/figma-to-code.md +298 -0
  100. package/assets/skills/file-upload.md +228 -0
  101. package/assets/skills/forms-patterns.md +264 -0
  102. package/assets/skills/gcp-patterns.md +189 -0
  103. package/assets/skills/git-workflow.md +187 -0
  104. package/assets/skills/golang-patterns.md +185 -0
  105. package/assets/skills/graphql-patterns.md +244 -0
  106. package/assets/skills/i18n-patterns.md +172 -0
  107. package/assets/skills/image-processing.md +350 -0
  108. package/assets/skills/java-springboot.md +226 -0
  109. package/assets/skills/kotlin-patterns.md +207 -0
  110. package/assets/skills/kubernetes-patterns.md +326 -0
  111. package/assets/skills/laravel-patterns.md +261 -0
  112. package/assets/skills/llm-fine-tuning.md +335 -0
  113. package/assets/skills/load-testing.md +303 -0
  114. package/assets/skills/logging-observability.md +228 -0
  115. package/assets/skills/markdown-processing.md +318 -0
  116. package/assets/skills/mcp-server-patterns.md +292 -0
  117. package/assets/skills/microservices.md +272 -0
  118. package/assets/skills/migration-patterns.md +239 -0
  119. package/assets/skills/mongodb-patterns.md +189 -0
  120. package/assets/skills/monorepo-patterns.md +287 -0
  121. package/assets/skills/nextjs-app-router.md +237 -0
  122. package/assets/skills/notification-patterns.md +348 -0
  123. package/assets/skills/oauth-patterns.md +246 -0
  124. package/assets/skills/payment-integration.md +222 -0
  125. package/assets/skills/pdf-generation.md +307 -0
  126. package/assets/skills/performance-optimization.md +277 -0
  127. package/assets/skills/php-patterns.md +210 -0
  128. package/assets/skills/prisma-patterns.md +241 -0
  129. package/assets/skills/prompt-engineering.md +193 -0
  130. package/assets/skills/pwa-patterns.md +247 -0
  131. package/assets/skills/python-patterns.md +158 -0
  132. package/assets/skills/python-testing.md +172 -0
  133. package/assets/skills/queue-patterns.md +295 -0
  134. package/assets/skills/rag-patterns.md +159 -0
  135. package/assets/skills/rate-limiting.md +319 -0
  136. package/assets/skills/react-components.md +201 -0
  137. package/assets/skills/react-native-patterns.md +299 -0
  138. package/assets/skills/real-time-patterns.md +181 -0
  139. package/assets/skills/redis-patterns.md +188 -0
  140. package/assets/skills/refactoring.md +218 -0
  141. package/assets/skills/regex-patterns.md +191 -0
  142. package/assets/skills/remix-patterns.md +262 -0
  143. package/assets/skills/responsive-design.md +199 -0
  144. package/assets/skills/ruby-rails-patterns.md +178 -0
  145. package/assets/skills/rust-patterns.md +211 -0
  146. package/assets/skills/search-patterns.md +227 -0
  147. package/assets/skills/security-hardening.md +237 -0
  148. package/assets/skills/seo-patterns.md +179 -0
  149. package/assets/skills/serverless-patterns.md +223 -0
  150. package/assets/skills/sql-optimization.md +154 -0
  151. package/assets/skills/state-management.md +254 -0
  152. package/assets/skills/storybook-patterns.md +330 -0
  153. package/assets/skills/svelte-patterns.md +258 -0
  154. package/assets/skills/swift-patterns.md +227 -0
  155. package/assets/skills/tailwind-patterns.md +272 -0
  156. package/assets/skills/tdd-workflow.md +199 -0
  157. package/assets/skills/terraform-patterns.md +270 -0
  158. package/assets/skills/testing-react.md +240 -0
  159. package/assets/skills/testing-vitest.md +232 -0
  160. package/assets/skills/typescript-strict.md +159 -0
  161. package/assets/skills/video-processing.md +340 -0
  162. package/assets/skills/vue-patterns.md +247 -0
  163. package/assets/skills/web-workers.md +327 -0
  164. package/assets/skills/webhooks-patterns.md +283 -0
  165. package/assets/skills/websocket-patterns.md +306 -0
  166. package/dist/cli/index.js +941 -958
  167. package/dist/core/index.d.ts +341 -11
  168. package/dist/core.js +58 -95
  169. package/dist/mcp/index.d.ts +33 -1
  170. package/dist/mcp-server.js +177 -255
  171. package/package.json +4 -1
@@ -0,0 +1,348 @@
1
+ ---
2
+ name: notification-patterns
3
+ description: Notification patterns for push, email, SMS, in-app notifications, user preference management, and batching strategies.
4
+ ---
5
+
6
+ # Notification Patterns
7
+
8
+ ## When to Use
9
+ Implement a notification system when your application needs to inform users about events through multiple channels: push notifications, email, SMS, and in-app alerts. These patterns cover channel abstraction, user preference management, batching to prevent notification fatigue, and reliable delivery with retry logic. Apply these patterns early to avoid building channel-specific silos that are hard to unify later.
10
+
11
+ ## How It Works
12
+
13
+ ### Notification Service Architecture
14
+
15
+ ```typescript
16
+ // src/notifications/types.ts
17
+ export interface Notification {
18
+ id: string;
19
+ userId: string;
20
+ type: NotificationType;
21
+ title: string;
22
+ body: string;
23
+ data?: Record<string, unknown>;
24
+ channels: Channel[];
25
+ priority: 'urgent' | 'high' | 'normal' | 'low';
26
+ groupKey?: string; // for batching related notifications
27
+ }
28
+
29
+ export type NotificationType =
30
+ | 'order.confirmed' | 'order.shipped' | 'order.delivered'
31
+ | 'comment.reply' | 'comment.mention'
32
+ | 'billing.invoice' | 'billing.failed'
33
+ | 'security.login' | 'security.password_changed';
34
+
35
+ export type Channel = 'push' | 'email' | 'sms' | 'in_app';
36
+
37
+ export interface ChannelProvider {
38
+ send(notification: Notification, recipient: Recipient): Promise<DeliveryResult>;
39
+ }
40
+
41
+ export interface DeliveryResult {
42
+ channel: Channel;
43
+ success: boolean;
44
+ externalId?: string;
45
+ error?: string;
46
+ }
47
+
48
+ export interface Recipient {
49
+ userId: string;
50
+ email?: string;
51
+ phone?: string;
52
+ pushTokens?: string[];
53
+ }
54
+ ```
55
+
56
+ ### Channel Providers
57
+
58
+ ```typescript
59
+ // src/notifications/channels/email.ts
60
+ import { Resend } from 'resend';
61
+
62
+ const resend = new Resend(process.env.RESEND_API_KEY);
63
+
64
+ export const emailProvider: ChannelProvider = {
65
+ async send(notification, recipient) {
66
+ if (!recipient.email) {
67
+ return { channel: 'email', success: false, error: 'No email address' };
68
+ }
69
+
70
+ try {
71
+ const result = await resend.emails.send({
72
+ from: 'MyApp <notifications@myapp.com>',
73
+ to: recipient.email,
74
+ subject: notification.title,
75
+ html: renderEmailTemplate(notification),
76
+ });
77
+
78
+ return { channel: 'email', success: true, externalId: result.id };
79
+ } catch (err) {
80
+ return { channel: 'email', success: false, error: (err as Error).message };
81
+ }
82
+ },
83
+ };
84
+
85
+ // src/notifications/channels/push.ts
86
+ import admin from 'firebase-admin';
87
+
88
+ export const pushProvider: ChannelProvider = {
89
+ async send(notification, recipient) {
90
+ if (!recipient.pushTokens?.length) {
91
+ return { channel: 'push', success: false, error: 'No push tokens' };
92
+ }
93
+
94
+ try {
95
+ const response = await admin.messaging().sendEachForMulticast({
96
+ tokens: recipient.pushTokens,
97
+ notification: { title: notification.title, body: notification.body },
98
+ data: notification.data as Record<string, string> ?? {},
99
+ android: { priority: notification.priority === 'urgent' ? 'high' : 'normal' },
100
+ apns: {
101
+ payload: { aps: { sound: notification.priority === 'urgent' ? 'default' : undefined } },
102
+ },
103
+ });
104
+
105
+ return {
106
+ channel: 'push',
107
+ success: response.successCount > 0,
108
+ error: response.failureCount > 0 ? `${response.failureCount} tokens failed` : undefined,
109
+ };
110
+ } catch (err) {
111
+ return { channel: 'push', success: false, error: (err as Error).message };
112
+ }
113
+ },
114
+ };
115
+
116
+ // src/notifications/channels/sms.ts
117
+ import twilio from 'twilio';
118
+
119
+ const client = twilio(process.env.TWILIO_SID, process.env.TWILIO_AUTH_TOKEN);
120
+
121
+ export const smsProvider: ChannelProvider = {
122
+ async send(notification, recipient) {
123
+ if (!recipient.phone) {
124
+ return { channel: 'sms', success: false, error: 'No phone number' };
125
+ }
126
+
127
+ try {
128
+ const msg = await client.messages.create({
129
+ body: `${notification.title}: ${notification.body}`,
130
+ from: process.env.TWILIO_PHONE_NUMBER,
131
+ to: recipient.phone,
132
+ });
133
+
134
+ return { channel: 'sms', success: true, externalId: msg.sid };
135
+ } catch (err) {
136
+ return { channel: 'sms', success: false, error: (err as Error).message };
137
+ }
138
+ },
139
+ };
140
+ ```
141
+
142
+ ### User Preference Management
143
+
144
+ ```typescript
145
+ // src/notifications/preferences.ts
146
+ interface NotificationPreferences {
147
+ userId: string;
148
+ channels: {
149
+ email: boolean;
150
+ push: boolean;
151
+ sms: boolean;
152
+ in_app: boolean;
153
+ };
154
+ types: Partial<Record<NotificationType, {
155
+ enabled: boolean;
156
+ channels?: Channel[];
157
+ }>>;
158
+ quietHours?: { start: string; end: string; timezone: string };
159
+ batchDigest?: 'none' | 'hourly' | 'daily';
160
+ }
161
+
162
+ export async function getEffectiveChannels(
163
+ userId: string,
164
+ type: NotificationType,
165
+ requestedChannels: Channel[]
166
+ ): Promise<Channel[]> {
167
+ const prefs = await getPreferences(userId);
168
+
169
+ return requestedChannels.filter((channel) => {
170
+ // Global channel toggle
171
+ if (!prefs.channels[channel]) return false;
172
+
173
+ // Per-type override
174
+ const typePrefs = prefs.types[type];
175
+ if (typePrefs?.enabled === false) return false;
176
+ if (typePrefs?.channels && !typePrefs.channels.includes(channel)) return false;
177
+
178
+ // Quiet hours (skip push/sms, keep email/in_app)
179
+ if (prefs.quietHours && (channel === 'push' || channel === 'sms')) {
180
+ if (isInQuietHours(prefs.quietHours)) return false;
181
+ }
182
+
183
+ return true;
184
+ });
185
+ }
186
+
187
+ function isInQuietHours(qh: { start: string; end: string; timezone: string }): boolean {
188
+ const now = new Date().toLocaleTimeString('en-US', {
189
+ hour12: false, timeZone: qh.timezone,
190
+ });
191
+ return now >= qh.start || now <= qh.end;
192
+ }
193
+ ```
194
+
195
+ ### Notification Dispatcher
196
+
197
+ ```typescript
198
+ // src/notifications/dispatcher.ts
199
+ import { emailProvider, pushProvider, smsProvider } from './channels';
200
+
201
+ const providers: Record<Channel, ChannelProvider> = {
202
+ email: emailProvider,
203
+ push: pushProvider,
204
+ sms: smsProvider,
205
+ in_app: inAppProvider,
206
+ };
207
+
208
+ export async function dispatch(notification: Notification): Promise<DeliveryResult[]> {
209
+ const recipient = await getRecipient(notification.userId);
210
+ const channels = await getEffectiveChannels(
211
+ notification.userId,
212
+ notification.type,
213
+ notification.channels
214
+ );
215
+
216
+ if (channels.length === 0) {
217
+ await logDelivery(notification.id, [{ channel: 'none' as Channel, success: false, error: 'All channels filtered by preferences' }]);
218
+ return [];
219
+ }
220
+
221
+ const results = await Promise.allSettled(
222
+ channels.map(async (channel) => {
223
+ const provider = providers[channel];
224
+ const result = await provider.send(notification, recipient);
225
+
226
+ await logDelivery(notification.id, [result]);
227
+ return result;
228
+ })
229
+ );
230
+
231
+ return results.map((r) =>
232
+ r.status === 'fulfilled' ? r.value : { channel: 'unknown' as Channel, success: false, error: (r.reason as Error).message }
233
+ );
234
+ }
235
+ ```
236
+
237
+ ### In-App Notifications with Real-Time
238
+
239
+ ```typescript
240
+ // src/notifications/channels/in-app.ts
241
+ export const inAppProvider: ChannelProvider = {
242
+ async send(notification, recipient) {
243
+ // Store in database
244
+ const stored = await db.query(
245
+ `INSERT INTO notifications (id, user_id, type, title, body, data, read, created_at)
246
+ VALUES ($1, $2, $3, $4, $5, $6, false, NOW()) RETURNING id`,
247
+ [notification.id, recipient.userId, notification.type, notification.title, notification.body, JSON.stringify(notification.data)]
248
+ );
249
+
250
+ // Push via WebSocket/SSE
251
+ emitToUser(recipient.userId, 'notification:new', {
252
+ id: stored.rows[0].id,
253
+ type: notification.type,
254
+ title: notification.title,
255
+ body: notification.body,
256
+ createdAt: new Date().toISOString(),
257
+ });
258
+
259
+ return { channel: 'in_app', success: true, externalId: stored.rows[0].id };
260
+ },
261
+ };
262
+
263
+ // API endpoints
264
+ app.get('/api/notifications', async (req, res) => {
265
+ const userId = req.userId;
266
+ const { cursor, limit = 20 } = req.query;
267
+
268
+ const notifications = await db.query(
269
+ `SELECT * FROM notifications WHERE user_id = $1
270
+ ${cursor ? 'AND created_at < $3' : ''}
271
+ ORDER BY created_at DESC LIMIT $2`,
272
+ cursor ? [userId, limit, cursor] : [userId, limit]
273
+ );
274
+
275
+ res.json({
276
+ items: notifications.rows,
277
+ unreadCount: await getUnreadCount(userId),
278
+ });
279
+ });
280
+
281
+ app.post('/api/notifications/:id/read', async (req, res) => {
282
+ await db.query('UPDATE notifications SET read = true WHERE id = $1 AND user_id = $2',
283
+ [req.params.id, req.userId]);
284
+ res.json({ success: true });
285
+ });
286
+
287
+ app.post('/api/notifications/read-all', async (req, res) => {
288
+ await db.query('UPDATE notifications SET read = true WHERE user_id = $1 AND read = false',
289
+ [req.userId]);
290
+ res.json({ success: true });
291
+ });
292
+ ```
293
+
294
+ ### Notification Batching
295
+
296
+ ```typescript
297
+ // src/notifications/batcher.ts
298
+ const pendingBatches = new Map<string, Notification[]>();
299
+
300
+ export function addToBatch(notification: Notification) {
301
+ const key = `${notification.userId}:${notification.groupKey}`;
302
+ const batch = pendingBatches.get(key) ?? [];
303
+ batch.push(notification);
304
+ pendingBatches.set(key, batch);
305
+ }
306
+
307
+ // Flush batches periodically
308
+ setInterval(async () => {
309
+ for (const [key, batch] of pendingBatches.entries()) {
310
+ if (batch.length === 0) continue;
311
+
312
+ pendingBatches.delete(key);
313
+
314
+ if (batch.length === 1) {
315
+ await dispatch(batch[0]);
316
+ } else {
317
+ // Merge into digest
318
+ const digest: Notification = {
319
+ ...batch[0],
320
+ title: `${batch.length} new ${batch[0].groupKey} notifications`,
321
+ body: batch.map((n) => n.title).join('\n'),
322
+ data: { count: batch.length, items: batch.map((n) => n.data) },
323
+ };
324
+ await dispatch(digest);
325
+ }
326
+ }
327
+ }, 5 * 60_000); // flush every 5 minutes
328
+ ```
329
+
330
+ ## Examples
331
+
332
+ | Notification Type | Channels | Priority | Batching |
333
+ |------------------|----------|----------|----------|
334
+ | `security.login` | push, email | urgent | Never |
335
+ | `order.shipped` | push, email, sms | high | Never |
336
+ | `comment.reply` | push, in_app | normal | 5-minute window |
337
+ | `comment.mention` | push, email, in_app | normal | 5-minute window |
338
+ | `billing.invoice` | email | normal | Never |
339
+
340
+ ## Checklist
341
+ - [ ] Channel abstraction allows adding new providers without changing dispatch logic
342
+ - [ ] User preferences control which channels and notification types are enabled
343
+ - [ ] Quiet hours suppress push and SMS during configured periods
344
+ - [ ] In-app notifications stored in database with real-time WebSocket delivery
345
+ - [ ] Batching groups related notifications to prevent alert fatigue
346
+ - [ ] Delivery results logged for debugging and analytics
347
+ - [ ] Failed deliveries retried via queue with exponential backoff
348
+ - [ ] Unsubscribe links included in all email notifications
@@ -0,0 +1,246 @@
1
+ ---
2
+ name: oauth-patterns
3
+ description: OAuth 2.0 patterns with authorization code flow, PKCE, scopes, token refresh, and social login.
4
+ ---
5
+
6
+ # OAuth 2.0 Patterns
7
+
8
+ ## When to Use
9
+ Apply when implementing "Sign in with Google/GitHub/etc." social login, building an API that third parties will authenticate against, or integrating with any service that uses OAuth 2.0. Always use Authorization Code flow with PKCE for web and mobile apps. Never use the Implicit flow -- it is deprecated and insecure.
10
+
11
+ ## How It Works
12
+
13
+ ### Authorization Code Flow with PKCE
14
+
15
+ The standard flow for web apps and SPAs:
16
+
17
+ ```typescript
18
+ import crypto from "crypto";
19
+
20
+ // Step 1: Generate PKCE verifier and challenge
21
+ function generatePKCE() {
22
+ const verifier = crypto.randomBytes(32).toString("base64url");
23
+ const challenge = crypto
24
+ .createHash("sha256")
25
+ .update(verifier)
26
+ .digest("base64url");
27
+ return { verifier, challenge };
28
+ }
29
+
30
+ // Step 2: Redirect user to authorization endpoint
31
+ app.get("/auth/login/google", (req, res) => {
32
+ const { verifier, challenge } = generatePKCE();
33
+ const state = crypto.randomBytes(16).toString("hex");
34
+
35
+ // Store verifier and state in session (not in URL)
36
+ req.session.pkceVerifier = verifier;
37
+ req.session.oauthState = state;
38
+
39
+ const params = new URLSearchParams({
40
+ client_id: process.env.GOOGLE_CLIENT_ID!,
41
+ redirect_uri: `${process.env.APP_URL}/auth/callback/google`,
42
+ response_type: "code",
43
+ scope: "openid email profile",
44
+ state,
45
+ code_challenge: challenge,
46
+ code_challenge_method: "S256",
47
+ });
48
+
49
+ res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
50
+ });
51
+
52
+ // Step 3: Handle the callback
53
+ app.get("/auth/callback/google", async (req, res) => {
54
+ const { code, state } = req.query;
55
+
56
+ // Verify state to prevent CSRF
57
+ if (state !== req.session.oauthState) {
58
+ return res.status(403).send("Invalid state parameter");
59
+ }
60
+
61
+ // Exchange code for tokens
62
+ const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
65
+ body: new URLSearchParams({
66
+ client_id: process.env.GOOGLE_CLIENT_ID!,
67
+ client_secret: process.env.GOOGLE_CLIENT_SECRET!,
68
+ code: code as string,
69
+ grant_type: "authorization_code",
70
+ redirect_uri: `${process.env.APP_URL}/auth/callback/google`,
71
+ code_verifier: req.session.pkceVerifier,
72
+ }),
73
+ });
74
+
75
+ const tokens = await tokenResponse.json();
76
+ // tokens: { access_token, refresh_token, id_token, expires_in }
77
+
78
+ // Step 4: Get user info from ID token or userinfo endpoint
79
+ const userInfo = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
80
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
81
+ }).then((r) => r.json());
82
+
83
+ // Step 5: Find or create user in your database
84
+ let user = await db.users.findByEmail(userInfo.email);
85
+ if (!user) {
86
+ user = await db.users.create({
87
+ email: userInfo.email,
88
+ name: userInfo.name,
89
+ avatarUrl: userInfo.picture,
90
+ provider: "google",
91
+ providerId: userInfo.sub,
92
+ });
93
+ }
94
+
95
+ // Step 6: Create your own session
96
+ req.session.userId = user.id;
97
+ res.redirect("/dashboard");
98
+ });
99
+ ```
100
+
101
+ ### Social Login with NextAuth.js (Auth.js)
102
+
103
+ ```typescript
104
+ // app/api/auth/[...nextauth]/route.ts
105
+ import NextAuth from "next-auth";
106
+ import GoogleProvider from "next-auth/providers/google";
107
+ import GitHubProvider from "next-auth/providers/github";
108
+ import { PrismaAdapter } from "@auth/prisma-adapter";
109
+ import { prisma } from "@/lib/prisma";
110
+
111
+ export const { handlers, auth, signIn, signOut } = NextAuth({
112
+ adapter: PrismaAdapter(prisma),
113
+ providers: [
114
+ GoogleProvider({
115
+ clientId: process.env.GOOGLE_CLIENT_ID!,
116
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
117
+ }),
118
+ GitHubProvider({
119
+ clientId: process.env.GITHUB_CLIENT_ID!,
120
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
121
+ }),
122
+ ],
123
+ callbacks: {
124
+ async session({ session, user }) {
125
+ session.user.id = user.id;
126
+ session.user.role = user.role;
127
+ return session;
128
+ },
129
+ },
130
+ });
131
+ ```
132
+
133
+ ### Token Refresh
134
+
135
+ ```typescript
136
+ async function refreshAccessToken(refreshToken: string): Promise<TokenSet> {
137
+ const response = await fetch("https://oauth2.googleapis.com/token", {
138
+ method: "POST",
139
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
140
+ body: new URLSearchParams({
141
+ client_id: process.env.GOOGLE_CLIENT_ID!,
142
+ client_secret: process.env.GOOGLE_CLIENT_SECRET!,
143
+ grant_type: "refresh_token",
144
+ refresh_token: refreshToken,
145
+ }),
146
+ });
147
+
148
+ if (!response.ok) {
149
+ throw new Error("Token refresh failed -- user must re-authenticate");
150
+ }
151
+
152
+ return response.json();
153
+ }
154
+
155
+ // Middleware that auto-refreshes expired tokens
156
+ async function ensureValidToken(req: Request): Promise<string> {
157
+ const session = await getSession(req);
158
+ if (session.accessTokenExpires > Date.now()) {
159
+ return session.accessToken;
160
+ }
161
+
162
+ const newTokens = await refreshAccessToken(session.refreshToken);
163
+ await updateSession(req, {
164
+ accessToken: newTokens.access_token,
165
+ accessTokenExpires: Date.now() + newTokens.expires_in * 1000,
166
+ refreshToken: newTokens.refresh_token ?? session.refreshToken,
167
+ });
168
+ return newTokens.access_token;
169
+ }
170
+ ```
171
+
172
+ ### Scopes -- Request Only What You Need
173
+
174
+ ```typescript
175
+ // Minimal scopes for login
176
+ const LOGIN_SCOPES = "openid email profile";
177
+
178
+ // Additional scopes requested later (progressive consent)
179
+ const CALENDAR_SCOPES = "https://www.googleapis.com/auth/calendar.readonly";
180
+ const DRIVE_SCOPES = "https://www.googleapis.com/auth/drive.file";
181
+
182
+ // Request additional scopes when the user needs the feature
183
+ app.get("/auth/connect/calendar", (req, res) => {
184
+ const params = new URLSearchParams({
185
+ client_id: process.env.GOOGLE_CLIENT_ID!,
186
+ redirect_uri: `${process.env.APP_URL}/auth/callback/google`,
187
+ response_type: "code",
188
+ scope: `${LOGIN_SCOPES} ${CALENDAR_SCOPES}`,
189
+ access_type: "offline",
190
+ prompt: "consent", // force consent screen for new scopes
191
+ login_hint: req.user.email,
192
+ });
193
+ res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
194
+ });
195
+ ```
196
+
197
+ ### Building Your Own OAuth Provider
198
+
199
+ ```typescript
200
+ // Issue authorization codes and access tokens for your API
201
+ import { randomBytes } from "crypto";
202
+
203
+ app.get("/oauth/authorize", async (req, res) => {
204
+ const { client_id, redirect_uri, scope, state, code_challenge } = req.query;
205
+
206
+ const client = await db.oauthClients.findById(client_id as string);
207
+ if (!client || !client.redirectUris.includes(redirect_uri as string)) {
208
+ return res.status(400).json({ error: "invalid_client" });
209
+ }
210
+
211
+ // Show consent screen, then on approval:
212
+ const code = randomBytes(32).toString("hex");
213
+ await db.authCodes.create({
214
+ code,
215
+ clientId: client_id as string,
216
+ userId: req.user.id,
217
+ scope: scope as string,
218
+ codeChallenge: code_challenge as string,
219
+ expiresAt: new Date(Date.now() + 10 * 60 * 1000),
220
+ });
221
+
222
+ const callbackUrl = new URL(redirect_uri as string);
223
+ callbackUrl.searchParams.set("code", code);
224
+ callbackUrl.searchParams.set("state", state as string);
225
+ res.redirect(callbackUrl.toString());
226
+ });
227
+ ```
228
+
229
+ ## Examples
230
+
231
+ | Flow | When | Security |
232
+ |------|------|----------|
233
+ | Authorization Code + PKCE | Web apps, SPAs, mobile | Best -- prevents code interception |
234
+ | Client Credentials | Server-to-server (no user) | Service accounts |
235
+ | Device Authorization | Smart TV, CLI tools | Limited input devices |
236
+ | Refresh Token | Long-lived sessions | Silent re-auth without login |
237
+
238
+ ## Checklist
239
+ - [ ] Authorization Code flow with PKCE used for all user-facing auth
240
+ - [ ] State parameter validated to prevent CSRF attacks
241
+ - [ ] PKCE verifier stored server-side (session), never in URL
242
+ - [ ] Refresh tokens stored securely (encrypted, httpOnly cookie or DB)
243
+ - [ ] Token refresh happens automatically before expiration
244
+ - [ ] Scopes requested incrementally (progressive consent)
245
+ - [ ] OAuth client secrets never exposed to the browser
246
+ - [ ] Redirect URIs validated against registered allowlist