naystack 1.1.16 → 1.2.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.
package/README.md ADDED
@@ -0,0 +1,1318 @@
1
+ # Naystack
2
+
3
+ > A stack built with tight **Next.js + Drizzle ORM + GraphQL** integration
4
+
5
+ [![npm version](https://img.shields.io/npm/v/naystack.svg)](https://www.npmjs.com/package/naystack)
6
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install naystack
12
+ # or
13
+ pnpm add naystack
14
+ # or
15
+ yarn add naystack
16
+ ```
17
+
18
+ ## Modules
19
+
20
+ Naystack provides the following modules, each accessible via its own import path:
21
+
22
+ | Module | Import Path | Description |
23
+ | ----------- | ------------------ | ----------------------------------------------- |
24
+ | **Auth** | `naystack/auth` | Email, Google, and Instagram authentication |
25
+ | **GraphQL** | `naystack/graphql` | GraphQL server initialization with type-graphql |
26
+ | **Client** | `naystack/client` | Client-side hooks and utilities |
27
+ | **File** | `naystack/file` | File upload to AWS S3 |
28
+ | **Socials** | `naystack/socials` | Instagram and Threads API integration |
29
+
30
+ ---
31
+
32
+ ## Auth Module
33
+
34
+ ```typescript
35
+ import {
36
+ getEmailAuthRoutes,
37
+ initGoogleAuth,
38
+ initInstagramAuth,
39
+ } from "naystack/auth";
40
+ ```
41
+
42
+ ### Email Authentication
43
+
44
+ Setup email-based authentication with JWT tokens and optional Turnstile captcha verification.
45
+
46
+ **Basic Example:**
47
+
48
+ ```typescript
49
+ const emailAuth = getEmailAuthRoutes({
50
+ getUser: async (email: string) => { /* fetch user by email */ },
51
+ createUser: async (user: UserInput) => { /* create new user */ },
52
+ signingKey: process.env.JWT_SIGNING_KEY!,
53
+ refreshKey: process.env.JWT_REFRESH_KEY!,
54
+ turnstileKey?: string, // Optional: Cloudflare Turnstile secret key
55
+ onSignUp: (user) => { /* callback on signup */ },
56
+ onLogout?: (body) => { /* callback on logout */ },
57
+ onError?: (error) => { /* custom error handler */ },
58
+ });
59
+
60
+ // Export in Next.js route handler
61
+ export const { GET, POST, PUT, DELETE, getUserIdFromRequest } = emailAuth;
62
+ ```
63
+
64
+ **Real-World Example with Drizzle ORM:**
65
+
66
+ ```typescript
67
+ import { db } from "@/lib/db";
68
+ import { UserTable, WebPushSubscriptionTable } from "@/lib/db/schema";
69
+ import { eq } from "drizzle-orm";
70
+ import { getEmailAuthRoutes } from "naystack/auth";
71
+ import { waitUntil } from "@vercel/functions";
72
+
73
+ export const { GET, POST, PUT, DELETE, getUserIdFromRequest } =
74
+ getEmailAuthRoutes({
75
+ // Fetch user by email using Drizzle
76
+ getUser: (email: string) =>
77
+ db.query.UserTable.findFirst({
78
+ where: eq(UserTable.email, email)
79
+ }),
80
+
81
+ // Create new user
82
+ createUser: async (user) => {
83
+ const [newUser] = await db
84
+ .insert(UserTable)
85
+ .values(user)
86
+ .returning();
87
+ return newUser;
88
+ },
89
+
90
+ signingKey: process.env.SIGNING_KEY!,
91
+ refreshKey: process.env.REFRESH_KEY!,
92
+ turnstileKey: process.env.TURNSTILE_KEY!,
93
+
94
+ // Send welcome email on signup
95
+ onSignUp: ({ id, email }) =>
96
+ waitUntil(
97
+ (async () => {
98
+ const link = await getVerificationLink(id);
99
+ if (link && email) {
100
+ await sendTemplateEmail(email, "WelcomeUser", {
101
+ verifyLink: link,
102
+ });
103
+ }
104
+ })(),
105
+ ),
106
+
107
+ // Clean up push subscriptions on logout
108
+ onLogout: async (endpoint: string) => {
109
+ await db
110
+ .delete(WebPushSubscriptionTable)
111
+ .where(eq(WebPushSubscriptionTable.endpoint, endpoint));
112
+ },
113
+ });
114
+ ```
115
+
116
+ #### Options
117
+
118
+ | Option | Type | Required | Description |
119
+ | -------------- | ------------------------------------------------------- | -------- | ------------------------------- |
120
+ | `getUser` | `(email: string) => Promise<UserOutput \| undefined>` | ✅ | Fetch user by email |
121
+ | `createUser` | `(user: UserInput) => Promise<UserOutput \| undefined>` | ✅ | Create new user |
122
+ | `signingKey` | `string` | ✅ | JWT signing key |
123
+ | `refreshKey` | `string` | ✅ | JWT refresh key |
124
+ | `turnstileKey` | `string` | ❌ | Cloudflare Turnstile secret key |
125
+ | `onSignUp` | `(user: UserOutput) => void` | ✅ | Callback when user signs up |
126
+ | `onLogout` | `(body: string) => Promise<void>` | ❌ | Callback on logout |
127
+ | `onError` | `ErrorHandler` | ❌ | Custom error handler |
128
+
129
+ #### Types
130
+
131
+ ```typescript
132
+ interface UserInput {
133
+ email: string;
134
+ password: string;
135
+ [key: string]: unknown;
136
+ }
137
+
138
+ interface UserOutput {
139
+ id: number;
140
+ email: string;
141
+ password: string | null;
142
+ [key: string]: unknown;
143
+ }
144
+ ```
145
+
146
+ ---
147
+
148
+ ### Google Authentication
149
+
150
+ **Real-World Example:**
151
+
152
+ ```typescript
153
+ import { db } from "@/lib/db";
154
+ import { UserTable } from "@/lib/db/schema";
155
+ import { eq } from "drizzle-orm";
156
+ import { initGoogleAuth } from "naystack/auth";
157
+
158
+ export const { GET } = initGoogleAuth({
159
+ clientId: process.env.GOOGLE_CLIENT_ID!,
160
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
161
+ authRoute: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/google`,
162
+ successRedirectURL: "/dashboard",
163
+ errorRedirectURL: "/login?error=google",
164
+ refreshKey: process.env.REFRESH_KEY!,
165
+
166
+ // Find existing user or create new one
167
+ getUserIdFromEmail: async ({ email, name }) => {
168
+ if (!email) return null;
169
+
170
+ // Update existing user's email verification status
171
+ const [existingUser] = await db
172
+ .update(UserTable)
173
+ .set({ emailVerified: true })
174
+ .where(eq(UserTable.email, email))
175
+ .returning({ id: UserTable.id });
176
+
177
+ if (existingUser) {
178
+ return existingUser.id;
179
+ }
180
+
181
+ // Create new user if doesn't exist
182
+ if (name && email) {
183
+ const [newUser] = await db
184
+ .insert(UserTable)
185
+ .values({
186
+ email,
187
+ name,
188
+ emailVerified: true,
189
+ })
190
+ .returning({ id: UserTable.id });
191
+
192
+ return newUser?.id || null;
193
+ }
194
+
195
+ return null;
196
+ },
197
+ });
198
+ ```
199
+
200
+ #### Options
201
+
202
+ | Option | Type | Required | Description |
203
+ | -------------------- | ----------------------------------------------------- | -------- | --------------------------------- |
204
+ | `clientId` | `string` | ✅ | Google OAuth client ID |
205
+ | `clientSecret` | `string` | ✅ | Google OAuth client secret |
206
+ | `authRoute` | `string` | ✅ | OAuth callback route |
207
+ | `successRedirectURL` | `string` | ✅ | Redirect URL on success |
208
+ | `errorRedirectURL` | `string` | ✅ | Redirect URL on error |
209
+ | `refreshKey` | `string` | ✅ | JWT refresh key |
210
+ | `getUserIdFromEmail` | `(email: Schema$Userinfo) => Promise<number \| null>` | ✅ | Get user ID from Google user info |
211
+
212
+ ---
213
+
214
+ ### Instagram Authentication
215
+
216
+ **Real-World Example:**
217
+
218
+ ```typescript
219
+ import { db } from "@/lib/db";
220
+ import { InstagramDetails, UserTable } from "@/lib/db/schema";
221
+ import { and, eq, ne } from "drizzle-orm";
222
+ import { initInstagramAuth } from "naystack/auth";
223
+
224
+ export const { GET, getRefreshedAccessToken } = initInstagramAuth({
225
+ clientId: process.env.NEXT_PUBLIC_INSTAGRAM_CLIENT_ID!,
226
+ clientSecret: process.env.INSTAGRAM_CLIENT_SECRET!,
227
+ authRoute: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/instagram`,
228
+ successRedirectURL: "/profile",
229
+ errorRedirectURL: "/signup",
230
+ refreshKey: process.env.REFRESH_KEY!,
231
+
232
+ // Verify and link Instagram account
233
+ onUser: async (instagramData, userId, accessToken) => {
234
+ if (!userId) return "You are not logged in";
235
+
236
+ const user = await db.query.UserTable.findFirst({
237
+ where: eq(UserTable.id, userId),
238
+ });
239
+
240
+ if (!user?.instagramDetails) {
241
+ return "Please connect Instagram first";
242
+ }
243
+
244
+ // Update Instagram verification status
245
+ const [updated] = await db
246
+ .update(InstagramDetails)
247
+ .set({
248
+ isVerified: true,
249
+ accessToken: accessToken
250
+ })
251
+ .where(
252
+ and(
253
+ eq(InstagramDetails.id, user.instagramDetails),
254
+ eq(InstagramDetails.username, instagramData.username),
255
+ ),
256
+ )
257
+ .returning({ username: InstagramDetails.username });
258
+
259
+ if (!updated) {
260
+ return "Please login with the same username as the connected account";
261
+ }
262
+
263
+ // Disconnect from other users if linked
264
+ await db
265
+ .update(UserTable)
266
+ .set({ instagramDetails: null })
267
+ .where(
268
+ and(
269
+ ne(UserTable.id, userId),
270
+ eq(UserTable.instagramDetails, user.instagramDetails),
271
+ ),
272
+ );
273
+ },
274
+ });
275
+ ```
276
+
277
+ #### Options
278
+
279
+ | Option | Type | Required | Description |
280
+ | -------------------- | ------------------------------------------------------------------------------------------- | -------- | --------------------------------- |
281
+ | `clientId` | `string` | ✅ | Instagram app client ID |
282
+ | `clientSecret` | `string` | ✅ | Instagram app client secret |
283
+ | `authRoute` | `string` | ✅ | OAuth callback route |
284
+ | `successRedirectURL` | `string` | ✅ | Redirect URL on success |
285
+ | `errorRedirectURL` | `string` | ✅ | Redirect URL on error |
286
+ | `refreshKey` | `string` | ✅ | JWT refresh key |
287
+ | `onUser` | `(data: InstagramUser, id: number \| null, accessToken: string) => Promise<string \| void>` | ✅ | Callback with Instagram user data |
288
+
289
+ ---
290
+
291
+ ## GraphQL Module
292
+
293
+ ```typescript
294
+ import {
295
+ initGraphQLServer,
296
+ GQLError,
297
+ query,
298
+ field,
299
+ QueryLibrary,
300
+ FieldLibrary,
301
+ } from "naystack/graphql";
302
+ import type { Context, AuthorizedContext } from "naystack/graphql";
303
+ ```
304
+
305
+ ### Initialize GraphQL Server
306
+
307
+ **Basic Example:**
308
+
309
+ ```typescript
310
+ const { GET, POST } = await initGraphQLServer({
311
+ resolvers: [UserResolver, PostResolver],
312
+ authChecker: ({ context }) => !!context.userId,
313
+ plugins: [], // Optional Apollo plugins
314
+ context: async (req) => ({
315
+ // Custom context builder
316
+ userId: await getUserIdFromRequest(req),
317
+ }),
318
+ });
319
+
320
+ export { GET, POST };
321
+ ```
322
+
323
+ **Real-World Example with Advanced Context:**
324
+
325
+ ```typescript
326
+ import { initGraphQLServer } from "naystack/graphql";
327
+ import { getUserIdFromRequest } from "@/api/(auth)/email/setup";
328
+ import { authChecker } from "@/lib/auth/context";
329
+
330
+ export const { GET, POST } = await initGraphQLServer({
331
+ resolvers: [
332
+ UserResolver,
333
+ PostResolver,
334
+ ApplicationResolver,
335
+ ChatResolver,
336
+ // ... more resolvers
337
+ ],
338
+ authChecker,
339
+ context: async (req) => {
340
+ const res = getUserIdFromRequest(req);
341
+ if (!res) return { userId: null };
342
+
343
+ // Handle refresh token user ID
344
+ if (res.refreshUserID) {
345
+ const isMobile = req.headers.get("x-platform-is-mobile");
346
+ if (isMobile) return { userId: null };
347
+ return { userId: res.refreshUserID, onlyQuery: true };
348
+ }
349
+
350
+ // Handle access token user ID
351
+ if (res.accessUserId) {
352
+ return { userId: res.accessUserId };
353
+ }
354
+
355
+ return { userId: null };
356
+ },
357
+ });
358
+ ```
359
+
360
+ #### Options
361
+
362
+ | Option | Type | Required | Description |
363
+ | ------------- | ------------------------------------ | -------- | -------------------------------- |
364
+ | `resolvers` | `NonEmptyArray<Function>` | ✅ | Array of TypeGraphQL resolvers |
365
+ | `authChecker` | `AuthChecker<any>` | ❌ | Custom auth checker function |
366
+ | `plugins` | `ApolloServerPlugin[]` | ❌ | Additional Apollo Server plugins |
367
+ | `context` | `(req: NextRequest) => Promise<any>` | ❌ | Context builder function |
368
+
369
+ ### Error Handling
370
+
371
+ **Basic Usage:**
372
+
373
+ ```typescript
374
+ import { GQLError } from "naystack/graphql";
375
+
376
+ // Usage in resolvers
377
+ throw GQLError(404, "User not found");
378
+ throw GQLError(403); // "You are not allowed to perform this action"
379
+ throw GQLError(400); // "Please provide all required inputs"
380
+ throw GQLError(); // "Server Error" (500)
381
+ ```
382
+
383
+ **Real-World Example in Resolvers:**
384
+
385
+ ```typescript
386
+ import { GQLError } from "naystack/graphql";
387
+ import { db } from "@/lib/db";
388
+ import { UserTable, PostingTable } from "@/lib/db/schema";
389
+ import { eq } from "drizzle-orm";
390
+
391
+ export async function createPosting(
392
+ ctx: Context,
393
+ input: NewPostingInput,
394
+ ): Promise<number | null> {
395
+ // Authentication check
396
+ if (!ctx.userId) {
397
+ throw GQLError(400, "Please login to create posting");
398
+ }
399
+
400
+ const user = await db.query.UserTable.findFirst({
401
+ where: eq(UserTable.id, ctx.userId),
402
+ });
403
+
404
+ // Authorization check
405
+ if (!user || user.role === "CREATOR") {
406
+ throw GQLError(400, "Only onboarded users can create postings");
407
+ }
408
+
409
+ // Validation check
410
+ if (!user.emailVerified) {
411
+ throw GQLError(400, "Please verify email to create posting");
412
+ }
413
+
414
+ // Rate limiting check
415
+ const yesterday = new Date();
416
+ yesterday.setDate(yesterday.getDate() - 1);
417
+ const recentPostings = await db.query.PostingTable.findMany({
418
+ where: and(
419
+ eq(PostingTable.userId, ctx.userId),
420
+ gte(PostingTable.createdAt, yesterday),
421
+ ),
422
+ });
423
+
424
+ if (recentPostings.length >= MAXIMUM_POSTINGS_DAY) {
425
+ throw GQLError(
426
+ 400,
427
+ `Only ${MAXIMUM_POSTINGS_DAY} allowed in 24 hours. Try again later.`,
428
+ );
429
+ }
430
+
431
+ // Create posting...
432
+ }
433
+ ```
434
+
435
+ ### Query & Field Helpers
436
+
437
+ Build resolvers functionally using `query`, `field`, `QueryLibrary`, and `FieldLibrary`:
438
+
439
+ **Basic Example:**
440
+
441
+ ```typescript
442
+ import { query, QueryLibrary, field, FieldLibrary } from "naystack/graphql";
443
+
444
+ // Define queries/mutations
445
+ const queries = {
446
+ getUser: query(
447
+ async (ctx, input) => {
448
+ return await db.query.users.findFirst({ where: eq(users.id, input) });
449
+ },
450
+ {
451
+ output: User,
452
+ input: Number,
453
+ authorized: true,
454
+ }
455
+ ),
456
+ createUser: query(
457
+ async (ctx, input) => {
458
+ /* ... */
459
+ },
460
+ {
461
+ output: User,
462
+ input: CreateUserInput,
463
+ mutation: true, // Makes this a mutation instead of query
464
+ }
465
+ ),
466
+ };
467
+
468
+ // Generate resolver class
469
+ const UserResolver = QueryLibrary(queries);
470
+
471
+ // Define field resolvers
472
+ const fields = {
473
+ posts: field(
474
+ async (root, ctx) => {
475
+ return await db.query.posts.findMany({
476
+ where: eq(posts.userId, root.id),
477
+ });
478
+ },
479
+ { output: [Post] }
480
+ ),
481
+ };
482
+
483
+ const UserFieldResolver = FieldLibrary(User, fields);
484
+ ```
485
+
486
+ **Real-World Example:**
487
+
488
+ ```typescript
489
+ import { query, QueryLibrary, field, FieldLibrary } from "naystack/graphql";
490
+ import { db } from "@/lib/db";
491
+ import { NotificationTable } from "@/lib/db/schema";
492
+ import { eq, lte } from "drizzle-orm";
493
+ import { waitUntil } from "@vercel/functions";
494
+
495
+ // Query with side effects
496
+ export const getNotifications = query(
497
+ async (ctx) => {
498
+ if (!ctx.userId) return [];
499
+
500
+ // Mark all as read
501
+ const notifications = await db
502
+ .update(NotificationTable)
503
+ .set({ read: true })
504
+ .where(eq(NotificationTable.user, ctx.userId))
505
+ .returning();
506
+
507
+ // Clean up old notifications (async)
508
+ const weekBefore = new Date();
509
+ weekBefore.setDate(weekBefore.getDate() - 7);
510
+ waitUntil(
511
+ db
512
+ .delete(NotificationTable)
513
+ .where(lte(NotificationTable.createdAt, weekBefore)),
514
+ );
515
+
516
+ return notifications.sort((a, b) => a.id - b.id);
517
+ },
518
+ {
519
+ output: [NotificationGQL!],
520
+ },
521
+ );
522
+
523
+ // Create resolver from queries
524
+ export const NotificationResolver = QueryLibrary({
525
+ getNotifications,
526
+ getUnreadNotifications,
527
+ });
528
+
529
+ // Field resolver example
530
+ export const UserFields = FieldLibrary(UserGQL, {
531
+ isOnboarded: field(
532
+ async (user) => {
533
+ return getIsOnboarded(user);
534
+ },
535
+ {
536
+ output: Boolean,
537
+ }
538
+ ),
539
+ });
540
+ ```
541
+
542
+ ### Types
543
+
544
+ ```typescript
545
+ interface Context {
546
+ userId: number | null;
547
+ }
548
+
549
+ interface AuthorizedContext {
550
+ userId: number;
551
+ }
552
+ ```
553
+
554
+ ---
555
+
556
+ ## Client Module
557
+
558
+ ```typescript
559
+ import {
560
+ useVisibility,
561
+ useBreakpoint,
562
+ setupSEO,
563
+ getHandleImageUpload,
564
+ getInstagramAuthorizationURLSetup,
565
+ } from "naystack/client";
566
+ ```
567
+
568
+ ### Hooks
569
+
570
+ #### `useVisibility`
571
+
572
+ Observe element visibility using Intersection Observer. Perfect for infinite scroll and lazy loading.
573
+
574
+ **Basic Example:**
575
+
576
+ ```typescript
577
+ function Component() {
578
+ const ref = useVisibility(() => {
579
+ console.log("Element is visible!");
580
+ });
581
+
582
+ return <div ref={ref}>Watch me!</div>;
583
+ }
584
+ ```
585
+
586
+ **Real-World Example - Infinite Scroll:**
587
+
588
+ ```typescript
589
+ "use client";
590
+
591
+ import { useVisibility } from "naystack/client";
592
+ import { useRef } from "react";
593
+
594
+ export default function PostingCard({
595
+ posting,
596
+ fetchMore
597
+ }: {
598
+ posting: Posting;
599
+ fetchMore?: () => void;
600
+ }) {
601
+ // Trigger fetchMore when card becomes visible
602
+ const mainRef = useVisibility(fetchMore);
603
+
604
+ return (
605
+ <div ref={mainRef} className="posting-card">
606
+ <h3>{posting.title}</h3>
607
+ <p>{posting.description}</p>
608
+ </div>
609
+ );
610
+ }
611
+ ```
612
+
613
+ #### `useBreakpoint`
614
+
615
+ React to media query changes. Useful for responsive layouts and conditional rendering.
616
+
617
+ **Basic Example:**
618
+
619
+ ```typescript
620
+ function Component() {
621
+ const isMobile = useBreakpoint("(max-width: 768px)");
622
+
623
+ return <div>{isMobile ? "Mobile" : "Desktop"}</div>;
624
+ }
625
+ ```
626
+
627
+ **Real-World Example - Responsive Layout:**
628
+
629
+ ```typescript
630
+ "use client";
631
+
632
+ import { useBreakpoint } from "naystack/client";
633
+
634
+ export default function LayoutWrapper({
635
+ children
636
+ }: {
637
+ children: React.ReactNode;
638
+ }) {
639
+ const isLarge = useBreakpoint("(min-width: 1024px)");
640
+
641
+ return (
642
+ <div
643
+ style={{
644
+ height: isLarge
645
+ ? "calc(100svh - 80px)"
646
+ : "calc(100svh - 55px)",
647
+ }}
648
+ className="flex flex-col"
649
+ >
650
+ {children}
651
+ </div>
652
+ );
653
+ }
654
+ ```
655
+
656
+ ### SEO Helper
657
+
658
+ ```typescript
659
+ const getSEO = setupSEO({
660
+ title: "My App",
661
+ description: "Default description",
662
+ siteName: "MyApp",
663
+ themeColor: "#000000",
664
+ });
665
+
666
+ // In page
667
+ export const metadata = getSEO(
668
+ "Page Title",
669
+ "Page description",
670
+ "/og-image.png"
671
+ );
672
+ ```
673
+
674
+ ### Instagram Authorization URL
675
+
676
+ Generate Instagram OAuth authorization URLs for client-side redirects.
677
+
678
+ **Example:**
679
+
680
+ ```typescript
681
+ import { getInstagramAuthorizationURLSetup } from "naystack/client";
682
+
683
+ // Setup once
684
+ const getInstagramAuthorizationURL = getInstagramAuthorizationURLSetup(
685
+ process.env.NEXT_PUBLIC_INSTAGRAM_CLIENT_ID!,
686
+ `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/instagram`,
687
+ );
688
+
689
+ // Usage in component
690
+ function ConnectInstagramButton() {
691
+ const handleConnect = () => {
692
+ const authURL = getInstagramAuthorizationURL(userToken);
693
+ window.location.href = authURL;
694
+ };
695
+
696
+ return <button onClick={handleConnect}>Connect Instagram</button>;
697
+ }
698
+ ```
699
+
700
+ ### Image Upload Client
701
+
702
+ ```typescript
703
+ const uploadImage = getHandleImageUpload("/api/upload");
704
+
705
+ const result = await uploadImage({
706
+ file: imageFile,
707
+ token: authToken,
708
+ type: "avatar",
709
+ data: { userId: 123 }, // Optional additional data
710
+ sync: true, // Optional: wait for processing
711
+ });
712
+ ```
713
+
714
+ ---
715
+
716
+ ## File Module
717
+
718
+ ```typescript
719
+ import { setupFileUpload } from "naystack/file";
720
+ ```
721
+
722
+ ### Setup File Upload
723
+
724
+ **Basic Example:**
725
+
726
+ ```typescript
727
+ const fileUpload = setupFileUpload({
728
+ refreshKey: process.env.JWT_REFRESH_KEY!,
729
+ signingKey: process.env.JWT_SIGNING_KEY!,
730
+ region: "us-east-1",
731
+ bucket: "my-bucket",
732
+ awsKey: process.env.AWS_ACCESS_KEY_ID!,
733
+ awsSecret: process.env.AWS_SECRET_ACCESS_KEY!,
734
+ processFile: async ({ url, type, userId, data }) => {
735
+ // Process uploaded file
736
+ return {
737
+ deleteURL: url, // URL to delete if needed
738
+ response: { success: true },
739
+ };
740
+ },
741
+ });
742
+
743
+ // Export route handler
744
+ export const { PUT } = fileUpload;
745
+
746
+ // Server-side utilities
747
+ const { getUploadFileURL, uploadImage, deleteImage, getFileURL, uploadFile } =
748
+ fileUpload;
749
+ ```
750
+
751
+ **Real-World Example with Multiple File Types:**
752
+
753
+ ```typescript
754
+ import { setupFileUpload } from "naystack/file";
755
+ import { db } from "@/lib/db";
756
+ import { PortfolioTable, UserTable } from "@/lib/db/schema";
757
+ import { and, eq } from "drizzle-orm";
758
+ import { waitUntil } from "@vercel/functions";
759
+
760
+ export const { deleteImage, getUploadFileURL, getFileURL, uploadImage, PUT } =
761
+ setupFileUpload({
762
+ region: process.env.SITE_AWS_REGION!,
763
+ refreshKey: process.env.REFRESH_KEY!,
764
+ awsSecret: process.env.SITE_AWS_SECRET_ACCESS_KEY!,
765
+ awsKey: process.env.SITE_AWS_ACCESS_KEY_ID!,
766
+ signingKey: process.env.SIGNING_KEY!,
767
+ bucket: process.env.SITE_AWS_BUCKET!,
768
+
769
+ processFile: async ({ url, userId, data, type }) => {
770
+ switch (type) {
771
+ case "PORTFOLIO":
772
+ // Handle portfolio image upload
773
+ waitUntil(
774
+ (async () => {
775
+ if (!url) return {};
776
+ const id = (data as { id?: number }).id;
777
+
778
+ if (id) {
779
+ // Update existing portfolio
780
+ const [existing] = await db
781
+ .select()
782
+ .from(PortfolioTable)
783
+ .where(
784
+ and(
785
+ eq(PortfolioTable.id, id),
786
+ eq(PortfolioTable.user, userId),
787
+ ),
788
+ );
789
+
790
+ if (!existing) {
791
+ return { deleteURL: url };
792
+ }
793
+
794
+ const oldURL = existing.imageURL;
795
+ await db
796
+ .update(PortfolioTable)
797
+ .set({ imageURL: url })
798
+ .where(
799
+ and(
800
+ eq(PortfolioTable.id, id),
801
+ eq(PortfolioTable.user, userId),
802
+ ),
803
+ );
804
+
805
+ return {
806
+ deleteURL: oldURL,
807
+ data: { id },
808
+ };
809
+ } else {
810
+ // Create new portfolio
811
+ const [portfolio] = await db
812
+ .insert(PortfolioTable)
813
+ .values({
814
+ user: userId,
815
+ imageURL: url,
816
+ caption: "",
817
+ link: "",
818
+ })
819
+ .returning({ id: PortfolioTable.id });
820
+
821
+ return { data: { id: portfolio?.id } };
822
+ }
823
+ })(),
824
+ );
825
+ break;
826
+
827
+ case "PROFILE_PICTURE":
828
+ // Handle profile picture upload
829
+ waitUntil(
830
+ (async () => {
831
+ const user = await db.query.UserTable.findFirst({
832
+ where: eq(UserTable.id, userId),
833
+ });
834
+
835
+ if (!user && url) {
836
+ return { deleteURL: url };
837
+ }
838
+
839
+ const oldPhoto = user?.photo;
840
+ await db
841
+ .update(UserTable)
842
+ .set({ photo: url })
843
+ .where(eq(UserTable.id, userId));
844
+
845
+ return {
846
+ deleteURL: oldPhoto || undefined,
847
+ };
848
+ })(),
849
+ );
850
+ break;
851
+ }
852
+
853
+ return {};
854
+ },
855
+ });
856
+ ```
857
+
858
+ #### Options
859
+
860
+ | Option | Type | Required | Description |
861
+ | ------------- | ---------- | -------- | ------------------------ |
862
+ | `refreshKey` | `string` | ✅ | JWT refresh key |
863
+ | `signingKey` | `string` | ✅ | JWT signing key |
864
+ | `region` | `string` | ✅ | AWS S3 region |
865
+ | `bucket` | `string` | ✅ | AWS S3 bucket name |
866
+ | `awsKey` | `string` | ✅ | AWS access key ID |
867
+ | `awsSecret` | `string` | ✅ | AWS secret access key |
868
+ | `processFile` | `Function` | ✅ | File processing callback |
869
+
870
+ #### Returned Utilities
871
+
872
+ | Utility | Description |
873
+ | ------------------ | ------------------------------ |
874
+ | `PUT` | Route handler for file uploads |
875
+ | `getUploadFileURL` | Get presigned URL for upload |
876
+ | `uploadImage` | Upload image to S3 |
877
+ | `deleteImage` | Delete image from S3 |
878
+ | `getFileURL` | Get public URL for a file |
879
+ | `uploadFile` | Upload any file to S3 |
880
+
881
+ ---
882
+
883
+ ## Socials Module
884
+
885
+ ```typescript
886
+ import {
887
+ // Instagram
888
+ getInstagramUser,
889
+ getInstagramMedia,
890
+ getInstagramConversations,
891
+ getInstagramConversationsByUser,
892
+ getInstagramConversationByUser,
893
+ getInstagramConversation,
894
+ getInstagramMessage,
895
+ sendInstagramMessage,
896
+ setupInstagramWebhook,
897
+ // Threads
898
+ getThread,
899
+ getThreads,
900
+ getThreadsReplies,
901
+ createThread,
902
+ createThreadsPost,
903
+ setupThreadsWebhook,
904
+ } from "naystack/socials";
905
+ ```
906
+
907
+ ### Instagram API
908
+
909
+ #### Get User Data
910
+
911
+ ```typescript
912
+ const user = await getInstagramUser(accessToken);
913
+ const user = await getInstagramUser(accessToken, "user_id");
914
+ const user = await getInstagramUser(accessToken, "me", [
915
+ "username",
916
+ "followers_count",
917
+ ]);
918
+ ```
919
+
920
+ #### Get Media
921
+
922
+ **Basic Usage:**
923
+
924
+ ```typescript
925
+ const media = await getInstagramMedia(accessToken);
926
+ const media = await getInstagramMedia(
927
+ accessToken,
928
+ ["like_count", "comments_count"],
929
+ 24
930
+ );
931
+ ```
932
+
933
+ **Real-World Example - Fetching Media with Custom Fields:**
934
+
935
+ ```typescript
936
+ import { getInstagramMedia } from "naystack/socials";
937
+
938
+ export async function fetchInstagramGraphMedia(
939
+ accessToken: string,
940
+ followers: number,
941
+ userId: number,
942
+ ) {
943
+ const fetchReq = await getInstagramMedia<{
944
+ thumbnail_url?: string;
945
+ id: string;
946
+ like_count?: number;
947
+ comments_count: number;
948
+ permalink: string;
949
+ caption: string;
950
+ media_url?: string;
951
+ media_type?: string;
952
+ timestamp: string;
953
+ }>(accessToken, [
954
+ "id",
955
+ "thumbnail_url",
956
+ "media_url",
957
+ "like_count",
958
+ "comments_count",
959
+ "media_type",
960
+ "permalink",
961
+ "caption",
962
+ "timestamp",
963
+ ]);
964
+
965
+ if (fetchReq?.data) {
966
+ return fetchReq.data.map((media) => ({
967
+ isVideo: media.media_type === "VIDEO",
968
+ comments: media.comments_count || -1,
969
+ likes: media.like_count || 0,
970
+ link: media.permalink,
971
+ thumbnail: media.thumbnail_url || media.media_url,
972
+ mediaURL: media.media_url,
973
+ timestamp: media.timestamp,
974
+ caption: media.caption,
975
+ appID: media.id,
976
+ user: userId,
977
+ er: calculateEngagementRate(
978
+ followers,
979
+ media.like_count || 0,
980
+ media.comments_count || -1,
981
+ ),
982
+ }));
983
+ }
984
+ }
985
+ ```
986
+
987
+ #### Conversations
988
+
989
+ ```typescript
990
+ // Get all conversations
991
+ const { data, fetchMore } = await getInstagramConversations(accessToken);
992
+
993
+ // Get conversations by user
994
+ const conversations = await getInstagramConversationsByUser(
995
+ accessToken,
996
+ userId
997
+ );
998
+
999
+ // Get single conversation
1000
+ const conversation = await getInstagramConversationByUser(accessToken, userId);
1001
+
1002
+ // Get conversation with messages
1003
+ const { messages, participants, fetchMore } = await getInstagramConversation(
1004
+ accessToken,
1005
+ conversationId
1006
+ );
1007
+ ```
1008
+
1009
+ #### Messages
1010
+
1011
+ ```typescript
1012
+ // Get message details
1013
+ const message = await getInstagramMessage(accessToken, messageId);
1014
+
1015
+ // Send message
1016
+ const result = await sendInstagramMessage(accessToken, recipientId, "Hello!");
1017
+ ```
1018
+
1019
+ #### Webhook
1020
+
1021
+ **Basic Example:**
1022
+
1023
+ ```typescript
1024
+ const instagramWebhook = setupInstagramWebhook({
1025
+ secret: process.env.INSTAGRAM_WEBHOOK_SECRET!,
1026
+ callback: async (type, value, id) => {
1027
+ // Handle webhook events
1028
+ },
1029
+ });
1030
+
1031
+ export const { GET, POST } = instagramWebhook;
1032
+ ```
1033
+
1034
+ **Real-World Example - Auto-Reply Bot:**
1035
+
1036
+ ```typescript
1037
+ import {
1038
+ getInstagramConversationByUser,
1039
+ sendInstagramMessage,
1040
+ setupInstagramWebhook,
1041
+ } from "naystack/socials";
1042
+
1043
+ export const { GET, POST } = setupInstagramWebhook({
1044
+ secret: process.env.REFRESH_KEY!,
1045
+ callback: async (
1046
+ type,
1047
+ value: {
1048
+ sender: { id: string };
1049
+ message: { text: string };
1050
+ recipient: { id: string };
1051
+ },
1052
+ ) => {
1053
+ if (
1054
+ type === "messaging" &&
1055
+ value.message.text &&
1056
+ value.sender.id !== "YOUR_PAGE_ID" &&
1057
+ value.recipient.id === "YOUR_PAGE_ID"
1058
+ ) {
1059
+ // Check if message is recent (within 24 hours)
1060
+ const conversation = await getInstagramConversationByUser(
1061
+ process.env.INSTAGRAM_ACCESS_TOKEN!,
1062
+ value.sender.id,
1063
+ );
1064
+
1065
+ const lastMessage = conversation?.messages?.data[1]?.created_time;
1066
+ if (lastMessage) {
1067
+ const lastMessageDate = new Date(lastMessage);
1068
+ if (lastMessageDate.getTime() > Date.now() - 1000 * 60 * 60 * 24) {
1069
+ return; // Already replied recently
1070
+ }
1071
+ }
1072
+
1073
+ // Generate reply (using your AI/LLM service)
1074
+ const reply = await generateReply(value.message.text);
1075
+ if (reply && reply !== '""') {
1076
+ await sendInstagramMessage(
1077
+ process.env.INSTAGRAM_ACCESS_TOKEN!,
1078
+ value.sender.id,
1079
+ reply,
1080
+ );
1081
+ }
1082
+ }
1083
+ },
1084
+ });
1085
+ ```
1086
+
1087
+ ---
1088
+
1089
+ ### Threads API
1090
+
1091
+ #### Get Threads
1092
+
1093
+ ```typescript
1094
+ // Get single thread
1095
+ const thread = await getThread(accessToken, threadId);
1096
+ const thread = await getThread(accessToken, threadId, ["text", "permalink"]);
1097
+
1098
+ // Get all threads
1099
+ const threads = await getThreads(accessToken);
1100
+
1101
+ // Get thread replies
1102
+ const replies = await getThreadsReplies(accessToken, threadId);
1103
+ ```
1104
+
1105
+ #### Create Threads
1106
+
1107
+ ```typescript
1108
+ // Create single post
1109
+ const postId = await createThreadsPost(accessToken, "Hello, Threads!");
1110
+
1111
+ // Reply to a thread
1112
+ const replyId = await createThreadsPost(
1113
+ accessToken,
1114
+ "Reply text",
1115
+ parentThreadId
1116
+ );
1117
+
1118
+ // Create threaded posts (carousel)
1119
+ const firstPostId = await createThread(accessToken, [
1120
+ "First post",
1121
+ "Second post in thread",
1122
+ "Third post in thread",
1123
+ ]);
1124
+ ```
1125
+
1126
+ #### Webhook
1127
+
1128
+ **Basic Example:**
1129
+
1130
+ ```typescript
1131
+ const threadsWebhook = setupThreadsWebhook({
1132
+ secret: process.env.THREADS_WEBHOOK_SECRET!,
1133
+ callback: async (type, value) => {
1134
+ // Handle webhook events
1135
+ return true; // Return false to indicate failure
1136
+ },
1137
+ });
1138
+
1139
+ export const { GET, POST } = threadsWebhook;
1140
+ ```
1141
+
1142
+ **Real-World Example - Auto-Reply to Threads:**
1143
+
1144
+ ```typescript
1145
+ import {
1146
+ createThreadsPost,
1147
+ getThreadsReplies,
1148
+ setupThreadsWebhook,
1149
+ } from "naystack/socials";
1150
+ import { db } from "@/lib/db";
1151
+ import { SocialPostsTable } from "@/lib/db/schema";
1152
+ import { eq } from "drizzle-orm";
1153
+
1154
+ const REPLIES = [
1155
+ "Thanks for your interest! Check out campaign ${ID}",
1156
+ "We'd love to work with you! See campaign ${ID}",
1157
+ ];
1158
+
1159
+ export const { GET, POST } = setupThreadsWebhook({
1160
+ secret: process.env.REFRESH_KEY!,
1161
+ callback: async (
1162
+ field,
1163
+ value: {
1164
+ id: string;
1165
+ username: string;
1166
+ text: string;
1167
+ replied_to: { id: string };
1168
+ root_post: { id: string; username: string };
1169
+ },
1170
+ ) => {
1171
+ // Skip if replying to own post
1172
+ if (value.root_post.username === value.username) return true;
1173
+
1174
+ // Only reply to direct replies to root post
1175
+ if (value.root_post.id !== value.replied_to.id) return true;
1176
+
1177
+ // Check if already replied
1178
+ const replies = await getThreadsReplies(
1179
+ process.env.THREADS_ACCESS_TOKEN!,
1180
+ value.id,
1181
+ );
1182
+ if (replies?.length ?? 0 > 0) return true;
1183
+
1184
+ // Find associated campaign
1185
+ const [post] = await db
1186
+ .select()
1187
+ .from(SocialPostsTable)
1188
+ .where(eq(SocialPostsTable.postID, value.root_post.id));
1189
+
1190
+ if (!post) return true;
1191
+
1192
+ // Generate reply message
1193
+ const message = REPLIES[
1194
+ Math.floor(Math.random() * REPLIES.length)
1195
+ ]?.replace("${ID}", post.campaignID.toString());
1196
+
1197
+ if (!message) return true;
1198
+
1199
+ // Send reply
1200
+ const res = await createThreadsPost(
1201
+ process.env.THREADS_ACCESS_TOKEN!,
1202
+ message,
1203
+ value.id,
1204
+ );
1205
+
1206
+ return !!res;
1207
+ },
1208
+ });
1209
+ ```
1210
+
1211
+ ---
1212
+
1213
+ ## Best Practices & Common Patterns
1214
+
1215
+ ### Authentication Flow
1216
+
1217
+ 1. **Email Auth with Database Integration:**
1218
+ - Use Drizzle ORM queries in `getUser` and `createUser`
1219
+ - Send verification emails in `onSignUp` callback
1220
+ - Clean up session data in `onLogout`
1221
+
1222
+ 2. **OAuth Integration:**
1223
+ - Always verify email/username matches existing accounts
1224
+ - Create new users only when necessary
1225
+ - Update verification status for existing users
1226
+
1227
+ ### GraphQL Resolver Patterns
1228
+
1229
+ 1. **Error Handling:**
1230
+ - Use `GQLError` for all error cases
1231
+ - Provide clear, user-friendly error messages
1232
+ - Use appropriate HTTP status codes (400, 403, 404, 500)
1233
+
1234
+ 2. **Authorization:**
1235
+ - Check authentication first (`if (!ctx.userId)`)
1236
+ - Verify permissions before operations
1237
+ - Use `authChecker` for role-based access control
1238
+
1239
+ 3. **Query Library Pattern:**
1240
+ - Use `query()` helper for simple queries/mutations
1241
+ - Use `QueryLibrary()` to combine multiple queries
1242
+ - Use `field()` and `FieldLibrary()` for computed fields
1243
+
1244
+ ### File Upload Patterns
1245
+
1246
+ 1. **Multiple File Types:**
1247
+ - Use `type` parameter to distinguish upload types
1248
+ - Handle each type in `processFile` callback
1249
+ - Return `deleteURL` for old files to clean up
1250
+
1251
+ 2. **Async Processing:**
1252
+ - Use `waitUntil()` for non-blocking operations
1253
+ - Process file metadata in background
1254
+ - Update database after successful upload
1255
+
1256
+ ### Webhook Patterns
1257
+
1258
+ 1. **Instagram Webhooks:**
1259
+ - Verify sender/recipient IDs
1260
+ - Check message timestamps to avoid duplicates
1261
+ - Use async operations for replies
1262
+
1263
+ 2. **Threads Webhooks:**
1264
+ - Filter by reply structure (root_post vs replied_to)
1265
+ - Check for existing replies before responding
1266
+ - Return `true`/`false` to indicate success/failure
1267
+
1268
+ ### Client-Side Patterns
1269
+
1270
+ 1. **Infinite Scroll:**
1271
+ - Use `useVisibility` hook with `fetchMore` callback
1272
+ - Attach ref to last item in list
1273
+ - Handle loading states
1274
+
1275
+ 2. **Responsive Design:**
1276
+ - Use `useBreakpoint` for conditional rendering
1277
+ - Adjust layout based on screen size
1278
+ - Combine with CSS for optimal UX
1279
+
1280
+ ---
1281
+
1282
+ ## Environment Variables
1283
+
1284
+ Here's a summary of common environment variables used:
1285
+
1286
+ ```env
1287
+ # JWT Keys
1288
+ JWT_SIGNING_KEY=your-signing-key
1289
+ JWT_REFRESH_KEY=your-refresh-key
1290
+
1291
+ # Google OAuth
1292
+ GOOGLE_CLIENT_ID=your-google-client-id
1293
+ GOOGLE_CLIENT_SECRET=your-google-client-secret
1294
+
1295
+ # Instagram
1296
+ INSTAGRAM_CLIENT_ID=your-instagram-client-id
1297
+ INSTAGRAM_CLIENT_SECRET=your-instagram-client-secret
1298
+ INSTAGRAM_WEBHOOK_SECRET=your-instagram-webhook-secret
1299
+
1300
+ # Threads
1301
+ THREADS_WEBHOOK_SECRET=your-threads-webhook-secret
1302
+
1303
+ # AWS S3
1304
+ AWS_ACCESS_KEY_ID=your-aws-key
1305
+ AWS_SECRET_ACCESS_KEY=your-aws-secret
1306
+
1307
+ # Cloudflare Turnstile (optional)
1308
+ TURNSTILE_SECRET_KEY=your-turnstile-key
1309
+
1310
+ # App
1311
+ NEXT_PUBLIC_BASE_URL=https://your-app.com
1312
+ ```
1313
+
1314
+ ---
1315
+
1316
+ ## License
1317
+
1318
+ ISC © [Abhinay Pandey](mailto:abhinaypandey02@gmail.com)