naystack 1.2.25 → 1.3.1

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