naystack 1.5.8 → 1.5.10

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 (114) hide show
  1. package/README.md +646 -91
  2. package/dist/auth/constants.d.mts +4 -0
  3. package/dist/auth/constants.d.ts +4 -0
  4. package/dist/auth/email/client.d.mts +149 -0
  5. package/dist/auth/email/client.d.ts +149 -0
  6. package/dist/auth/email/index.cjs.js +2 -14
  7. package/dist/auth/email/index.d.mts +41 -1
  8. package/dist/auth/email/index.d.ts +41 -1
  9. package/dist/auth/email/index.esm.js +1 -12
  10. package/dist/auth/email/routes/delete.cjs.js +0 -1
  11. package/dist/auth/email/routes/delete.d.mts +5 -0
  12. package/dist/auth/email/routes/delete.d.ts +5 -0
  13. package/dist/auth/email/routes/delete.esm.js +0 -1
  14. package/dist/auth/email/routes/get.d.mts +5 -0
  15. package/dist/auth/email/routes/get.d.ts +5 -0
  16. package/dist/auth/email/routes/post.cjs.js +0 -1
  17. package/dist/auth/email/routes/post.d.mts +5 -0
  18. package/dist/auth/email/routes/post.d.ts +5 -0
  19. package/dist/auth/email/routes/post.esm.js +0 -1
  20. package/dist/auth/email/routes/put.cjs.js +0 -1
  21. package/dist/auth/email/routes/put.d.mts +5 -0
  22. package/dist/auth/email/routes/put.d.ts +5 -0
  23. package/dist/auth/email/routes/put.esm.js +0 -1
  24. package/dist/auth/email/token.d.mts +62 -0
  25. package/dist/auth/email/token.d.ts +62 -0
  26. package/dist/auth/email/types.d.mts +22 -0
  27. package/dist/auth/email/types.d.ts +22 -0
  28. package/dist/auth/email/utils.cjs.js +0 -12
  29. package/dist/auth/email/utils.d.mts +41 -2
  30. package/dist/auth/email/utils.d.ts +41 -2
  31. package/dist/auth/email/utils.esm.js +0 -11
  32. package/dist/auth/google/get.d.mts +5 -0
  33. package/dist/auth/google/get.d.ts +5 -0
  34. package/dist/auth/google/index.d.mts +39 -0
  35. package/dist/auth/google/index.d.ts +39 -0
  36. package/dist/auth/index.cjs.js +4 -16
  37. package/dist/auth/index.d.mts +1 -1
  38. package/dist/auth/index.d.ts +1 -1
  39. package/dist/auth/index.esm.js +3 -14
  40. package/dist/auth/instagram/client.d.mts +19 -0
  41. package/dist/auth/instagram/client.d.ts +19 -0
  42. package/dist/auth/instagram/index.d.mts +37 -0
  43. package/dist/auth/instagram/index.d.ts +37 -0
  44. package/dist/auth/instagram/route.d.mts +5 -0
  45. package/dist/auth/instagram/route.d.ts +5 -0
  46. package/dist/auth/instagram/utils.d.mts +13 -0
  47. package/dist/auth/instagram/utils.d.ts +13 -0
  48. package/dist/auth/types.d.mts +24 -0
  49. package/dist/auth/types.d.ts +24 -0
  50. package/dist/auth/utils/errors.d.mts +10 -0
  51. package/dist/auth/utils/errors.d.ts +10 -0
  52. package/dist/auth/utils/token.d.mts +20 -0
  53. package/dist/auth/utils/token.d.ts +20 -0
  54. package/dist/client/hooks.d.mts +59 -0
  55. package/dist/client/hooks.d.ts +59 -0
  56. package/dist/client/seo.d.mts +46 -0
  57. package/dist/client/seo.d.ts +46 -0
  58. package/dist/env.d.mts +61 -0
  59. package/dist/env.d.ts +61 -0
  60. package/dist/file/client.d.mts +53 -1
  61. package/dist/file/client.d.ts +53 -1
  62. package/dist/file/index.cjs.js +0 -1
  63. package/dist/file/index.esm.js +0 -1
  64. package/dist/file/put.cjs.js +0 -1
  65. package/dist/file/put.d.mts +11 -0
  66. package/dist/file/put.d.ts +11 -0
  67. package/dist/file/put.esm.js +0 -1
  68. package/dist/file/setup.cjs.js +0 -1
  69. package/dist/file/setup.d.mts +48 -0
  70. package/dist/file/setup.d.ts +48 -0
  71. package/dist/file/setup.esm.js +0 -1
  72. package/dist/file/utils.d.mts +41 -0
  73. package/dist/file/utils.d.ts +41 -0
  74. package/dist/graphql/client.d.mts +113 -0
  75. package/dist/graphql/client.d.ts +113 -0
  76. package/dist/graphql/errors.d.mts +26 -0
  77. package/dist/graphql/errors.d.ts +26 -0
  78. package/dist/graphql/index.cjs.js +2 -3
  79. package/dist/graphql/index.esm.js +2 -3
  80. package/dist/graphql/init.cjs.js +0 -1
  81. package/dist/graphql/init.d.mts +33 -0
  82. package/dist/graphql/init.d.ts +33 -0
  83. package/dist/graphql/init.esm.js +0 -1
  84. package/dist/graphql/server.d.mts +88 -0
  85. package/dist/graphql/server.d.ts +88 -0
  86. package/dist/graphql/types.d.mts +21 -0
  87. package/dist/graphql/types.d.ts +21 -0
  88. package/dist/graphql/utils.d.mts +217 -0
  89. package/dist/graphql/utils.d.ts +217 -0
  90. package/dist/index.d.mts +16 -0
  91. package/dist/index.d.ts +16 -0
  92. package/dist/socials/instagram/getters.d.mts +115 -0
  93. package/dist/socials/instagram/getters.d.ts +115 -0
  94. package/dist/socials/instagram/setters.d.mts +18 -0
  95. package/dist/socials/instagram/setters.d.ts +18 -0
  96. package/dist/socials/instagram/types.d.mts +46 -0
  97. package/dist/socials/instagram/types.d.ts +46 -0
  98. package/dist/socials/instagram/utils.d.mts +19 -0
  99. package/dist/socials/instagram/utils.d.ts +19 -0
  100. package/dist/socials/instagram/webhook.d.mts +31 -0
  101. package/dist/socials/instagram/webhook.d.ts +31 -0
  102. package/dist/socials/meta-webhook.d.mts +11 -0
  103. package/dist/socials/meta-webhook.d.ts +11 -0
  104. package/dist/socials/threads/getters.d.mts +57 -0
  105. package/dist/socials/threads/getters.d.ts +57 -0
  106. package/dist/socials/threads/setters.d.mts +59 -0
  107. package/dist/socials/threads/setters.d.ts +59 -0
  108. package/dist/socials/threads/types.d.mts +9 -0
  109. package/dist/socials/threads/types.d.ts +9 -0
  110. package/dist/socials/threads/utils.d.mts +19 -0
  111. package/dist/socials/threads/utils.d.ts +19 -0
  112. package/dist/socials/threads/webhook.d.mts +30 -0
  113. package/dist/socials/threads/webhook.d.ts +30 -0
  114. package/package.json +4 -2
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # Naystack
2
2
 
3
- A minimal, powerful stack for Next.js app development. Built with **Next.js + Drizzle ORM + GraphQL + S3 Auth**.
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)
7
7
 
8
+ **[API Reference](https://abhinaypandey02.github.io/naystack/)**
9
+
8
10
  ## Installation
9
11
 
10
12
  ```bash
@@ -15,11 +17,11 @@ pnpm add naystack
15
17
 
16
18
  ## 1. Authentication
17
19
 
18
- Naystack provides a seamless email-based authentication system with optional support for Google and Instagram.
20
+ Naystack provides a seamless email-based authentication system with optional support for Google and Instagram OAuth.
19
21
 
20
22
  ### Server Setup
21
23
 
22
- Define your auth routes in `app/api/(auth)/email/index.ts`:
24
+ Define your auth routes in `app/api/(auth)/email/route.ts`. The library reads `SIGNING_KEY` and `REFRESH_KEY` from environment variables automatically.
23
25
 
24
26
  ```typescript
25
27
  import { getEmailAuthRoutes } from "naystack/auth";
@@ -27,109 +29,495 @@ import { db } from "@/app/api/lib/db";
27
29
  import { UserTable } from "@/app/api/(graphql)/User/db";
28
30
  import { eq } from "drizzle-orm";
29
31
 
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) => {
32
+ export const { GET, POST, PUT, DELETE } = getEmailAuthRoutes({
33
+ // Fetch user by request data (used for login & sign-up duplicate check)
34
+ getUser: async ({ email }: { email: string }) => {
36
35
  const [user] = await db
37
- .select({
38
- id: UserTable.id,
39
- email: UserTable.email,
40
- password: UserTable.password,
41
- })
36
+ .select({ id: UserTable.id, password: UserTable.password })
42
37
  .from(UserTable)
43
38
  .where(eq(UserTable.email, email));
44
39
  return user;
45
40
  },
46
- keys: {
47
- signing: process.env.SIGNING_KEY!,
48
- refresh: process.env.REFRESH_KEY!,
41
+ // Create a new user with the hashed password
42
+ createUser: async (data: { email: string; password: string; name: string }) => {
43
+ const [user] = await db
44
+ .insert(UserTable)
45
+ .values(data)
46
+ .returning({ id: UserTable.id, password: UserTable.password });
47
+ return user;
48
+ },
49
+ // Optional: callback after successful sign-up
50
+ onSignUp: async (userId, body: { orgTitle?: string }) => {
51
+ if (body.orgTitle && userId) {
52
+ await createOrg(userId, { title: body.orgTitle });
53
+ }
49
54
  },
50
55
  });
51
56
  ```
52
57
 
53
- > **Note:** Google and Instagram auth are also available via `initGoogleAuth` and `initInstagramAuth` from `naystack/auth`.
58
+ The returned route handlers map to:
59
+
60
+ | Handler | HTTP Method | Purpose |
61
+ | -------- | ----------- | ----------------------------------------- |
62
+ | `GET` | GET | Refresh tokens (exchange refresh cookie) |
63
+ | `POST` | POST | Sign up (create user, return tokens) |
64
+ | `PUT` | PUT | Login (verify credentials, return tokens) |
65
+ | `DELETE` | DELETE | Logout (clear refresh cookie) |
54
66
 
55
67
  ### Client Setup
56
68
 
57
- Wrap your application with `AuthWrapper` in your root layout or a client component.
69
+ Wrap your application with `AuthWrapper` in your root layout. This fetches the access token on mount and provides it to all auth hooks via React context.
58
70
 
59
- ```typescript
60
- // gql/client.ts
61
- "use client";
71
+ ```tsx
72
+ // app/layout.tsx
62
73
  import { AuthWrapper } from "naystack/auth/email/client";
74
+ import { ApolloWrapper } from "naystack/graphql/client";
63
75
 
76
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
77
+ return (
78
+ <html lang="en">
79
+ <body>
80
+ <AuthWrapper>
81
+ <ApolloWrapper>{children}</ApolloWrapper>
82
+ </AuthWrapper>
83
+ </body>
84
+ </html>
85
+ );
86
+ }
64
87
  ```
65
88
 
66
- ### Frontend Usage
89
+ ### Frontend Hooks
67
90
 
68
- ```typescript
69
- import { useLogin, useSignUp, useLogout, useToken } from "naystack/auth/email/client";
91
+ #### `useToken()`
92
+
93
+ Returns the current JWT access token (or `null` if not loaded / logged out). Use it for conditional rendering or passing to custom fetch calls.
94
+
95
+ ```tsx
96
+ import { useToken } from "naystack/auth/email/client";
97
+
98
+ export default function Home() {
99
+ const token = useToken();
100
+
101
+ return (
102
+ <Link href={token ? "/dashboard" : "/signup"}>
103
+ <button>{token ? "Dashboard" : "Get Started"}</button>
104
+ </Link>
105
+ );
106
+ }
107
+ ```
108
+
109
+ #### `useSignUp()`
110
+
111
+ Returns a function that registers a new user. Sends a POST to the auth endpoint. Returns `null` on success, or the error message string on failure.
112
+
113
+ ```tsx
114
+ import { useSignUp } from "naystack/auth/email/client";
115
+
116
+ function SignUpForm() {
117
+ const signUp = useSignUp();
118
+
119
+ const handleSubmit = async (data: { name: string; email: string; password: string }) => {
120
+ const error = await signUp(data);
121
+ if (error) {
122
+ setMessage(error);
123
+ } else {
124
+ router.replace("/dashboard");
125
+ }
126
+ };
127
+ }
128
+ ```
129
+
130
+ #### `useLogin()`
70
131
 
132
+ Returns a function that logs the user in. Sends a PUT to the auth endpoint. Returns `null` on success, or the error message string on failure.
71
133
 
72
- function AuthComponent() {
134
+ ```tsx
135
+ import { useLogin } from "naystack/auth/email/client";
136
+
137
+ function LoginForm() {
73
138
  const login = useLogin();
74
- const signup = useSignUp();
139
+
140
+ const handleSubmit = async (data: { email: string; password: string }) => {
141
+ const error = await login(data);
142
+ if (error) {
143
+ form.setError("password", { message: error });
144
+ } else {
145
+ router.replace("/dashboard");
146
+ }
147
+ };
148
+ }
149
+ ```
150
+
151
+ #### `useLogout()`
152
+
153
+ Returns a function that logs the user out. Clears the token immediately and sends DELETE to the auth endpoint.
154
+
155
+ ```tsx
156
+ import { useLogout } from "naystack/auth/email/client";
157
+
158
+ function LogoutButton() {
75
159
  const logout = useLogout();
76
- const token = useToken(); // Get current JWT token
77
160
 
78
- // ... forms and handlers
161
+ return (
162
+ <button onClick={() => { logout(); router.push("/login"); }}>
163
+ Log out
164
+ </button>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ### Server-side Auth Helpers
170
+
171
+ #### `getContext(req)`
172
+
173
+ Extracts the auth context from a `NextRequest`. Reads either the `Authorization: Bearer <token>` header or the refresh cookie. Use it in API routes outside of GraphQL.
174
+
175
+ ```typescript
176
+ import { getContext } from "naystack/auth";
177
+
178
+ export const POST = async (req: NextRequest) => {
179
+ const ctx = getContext(req);
180
+ if (!ctx?.userId) return new NextResponse("Unauthorized", { status: 401 });
181
+
182
+ // ctx.userId is available for authenticated operations
183
+ const chats = await db.select().from(ChatTable).where(eq(ChatTable.userId, ctx.userId));
184
+ return NextResponse.json(chats);
185
+ };
186
+ ```
187
+
188
+ #### `getRefreshToken()`
189
+
190
+ Server-side function to read the refresh token from cookies. Useful in Server Components and layouts to check if the user is logged in.
191
+
192
+ ```typescript
193
+ import { getRefreshToken } from "naystack/auth";
194
+ import { redirect } from "next/navigation";
195
+
196
+ export default async function ProtectedLayout({ children }: { children: React.ReactNode }) {
197
+ const token = await getRefreshToken();
198
+ if (!token) return redirect("/login");
199
+ return <div>{children}</div>;
79
200
  }
80
201
  ```
81
202
 
203
+ #### `checkAuthStatus(redirectURL?)`
204
+
205
+ Checks if the current request has a valid refresh cookie. Optionally redirects to the given URL if not authorized.
206
+
207
+ ```typescript
208
+ import { checkAuthStatus } from "naystack/auth";
209
+
210
+ // In a Server Component:
211
+ await checkAuthStatus("/login"); // Redirects to /login if not authorized
212
+ ```
213
+
214
+ ### Google OAuth
215
+
216
+ ```typescript
217
+ import { initGoogleAuth } from "naystack/auth";
218
+
219
+ export const { GET } = initGoogleAuth({
220
+ getUserIdFromEmail: async (googleUser) => {
221
+ // Find or create user by Google email
222
+ return findOrCreateUserByEmail(googleUser.email!);
223
+ },
224
+ redirectURL: "/dashboard",
225
+ errorRedirectURL: "/login",
226
+ });
227
+ ```
228
+
229
+ ### Instagram OAuth
230
+
231
+ ```typescript
232
+ import { initInstagramAuth } from "naystack/auth";
233
+
234
+ export const { GET, getRefreshedAccessToken } = initInstagramAuth({
235
+ onUser: async (igUser, appUserId, accessToken) => {
236
+ await saveInstagramUser(appUserId, igUser, accessToken);
237
+ },
238
+ successRedirectURL: "/dashboard",
239
+ errorRedirectURL: "/login",
240
+ refreshKey: process.env.REFRESH_KEY!,
241
+ });
242
+ ```
243
+
82
244
  ---
83
245
 
84
246
  ## 2. GraphQL
85
247
 
86
- Naystack leverages `type-graphql` and `apollo-server` for a type-safe GraphQL experience.
248
+ Naystack provides a type-safe GraphQL layer built on `type-graphql` and `Apollo Server`. Define resolvers as plain functions and let the library generate the schema.
87
249
 
88
- ### Server Setup
250
+ ### Defining Queries and Mutations
251
+
252
+ Use `query()` to define a resolver. It returns an object with the resolver function, plus `.call()` and `.authCall()` for direct server-side invocation (e.g. in Server Components).
253
+
254
+ ```typescript
255
+ // app/api/(graphql)/User/resolvers/get-current-user.ts
256
+ import { query } from "naystack/graphql";
257
+
258
+ export default query(
259
+ async (ctx) => {
260
+ if (!ctx.userId) return null;
261
+ const [user] = await db
262
+ .select()
263
+ .from(UserTable)
264
+ .where(eq(UserTable.id, ctx.userId));
265
+ return user || null;
266
+ },
267
+ {
268
+ output: User, // GraphQL return type (type-graphql class)
269
+ outputOptions: { nullable: true }, // Return type is nullable
270
+ },
271
+ );
272
+ ```
273
+
274
+ **With input and authorization:**
275
+
276
+ ```typescript
277
+ // app/api/(graphql)/Feedback/resolvers/submit-feedback.ts
278
+ import { query } from "naystack/graphql";
279
+
280
+ export default query(
281
+ async (ctx, input: SubmitFeedbackInput) => {
282
+ await db.insert(FeedbackTable).values({
283
+ userId: ctx.userId, // guaranteed non-null when authorized: true
284
+ score: input.score,
285
+ text: input.text,
286
+ });
287
+ return true;
288
+ },
289
+ {
290
+ output: Boolean,
291
+ input: SubmitFeedbackInput, // GraphQL input type (type-graphql @InputType class)
292
+ authorized: true, // Requires authenticated user (ctx.userId non-null)
293
+ mutation: true, // Registers as a Mutation (default is Query)
294
+ },
295
+ );
296
+ ```
297
+
298
+ ### Defining Field Resolvers
299
+
300
+ Use `field()` to define resolvers for computed fields on a parent type. The first argument is the parent object.
301
+
302
+ ```typescript
303
+ // app/api/(graphql)/Property/resolvers/seller-field.ts
304
+ import { field } from "naystack/graphql";
305
+
306
+ export default field(
307
+ async (property: PropertyDB) => {
308
+ if (!property.sellerId) return null;
309
+ const [seller] = await db
310
+ .select()
311
+ .from(ContactTable)
312
+ .where(eq(ContactTable.id, property.sellerId));
313
+ return seller || null;
314
+ },
315
+ {
316
+ output: ContactGQL,
317
+ outputOptions: { nullable: true },
318
+ },
319
+ );
320
+ ```
89
321
 
90
- Initialize your GraphQL server in `app/api/(graphql)/route.ts`:
322
+ ### Registering Resolvers
323
+
324
+ Use `QueryLibrary()` for queries/mutations and `FieldLibrary()` for field resolvers. Pass the result to `initGraphQLServer`.
91
325
 
92
326
  ```typescript
327
+ // app/api/(graphql)/User/graphql.ts
328
+ import { QueryLibrary, FieldLibrary } from "naystack/graphql";
329
+ import getCurrentUser from "./resolvers/get-current-user";
330
+ import onboardUser from "./resolvers/onboard-user";
331
+ import updateUser from "./resolvers/update-user";
332
+ import organizations from "./resolvers/organizations-field";
333
+ import { User } from "./types";
334
+
335
+ // Each key becomes a Query or Mutation field name in the schema
336
+ export const UserResolvers = QueryLibrary({
337
+ getCurrentUser,
338
+ onboardUser,
339
+ updateUser,
340
+ });
341
+
342
+ // Each key becomes a field resolver on the User type
343
+ export const UserFieldResolvers = FieldLibrary<UserDB>(User, {
344
+ organizations,
345
+ });
346
+ ```
347
+
348
+ ### Initializing the GraphQL Server
349
+
350
+ ```typescript
351
+ // app/api/(graphql)/route.ts
93
352
  import { initGraphQLServer } from "naystack/graphql";
94
- import { getContext } from "@/app/api/(auth)/email";
95
- import { UserResolvers } from "./User/graphql";
353
+ import { UserResolvers, UserFieldResolvers } from "./User/graphql";
354
+ import { ChatResolvers } from "./Chat/graphql";
355
+ import { FeedbackResolvers } from "./Feedback/graphql";
96
356
 
97
357
  export const { GET, POST } = await initGraphQLServer({
98
- getContext,
99
- resolvers: [UserResolvers],
358
+ resolvers: [UserResolvers, UserFieldResolvers, ChatResolvers, FeedbackResolvers],
100
359
  });
101
360
  ```
102
361
 
103
- ### Server Component Query
362
+ The `getContext` function is built in — it reads the `Authorization` header or refresh cookie automatically. Pass a custom `getContext` if you need to override it.
363
+
364
+ ### Throwing Errors
365
+
366
+ Use `GQLError()` to throw structured GraphQL errors from resolvers:
367
+
368
+ ```typescript
369
+ import { GQLError } from "naystack/graphql";
370
+
371
+ // In a resolver:
372
+ if (!input.email) throw GQLError(400); // "Please provide all required inputs"
373
+ if (!ctx.userId) throw GQLError(403); // "You are not allowed to perform this action"
374
+ if (!deal) throw GQLError(404, "Deal not found"); // Custom message
375
+ ```
376
+
377
+ ### Type Helper: `QueryResponseType`
378
+
379
+ Infer the return type of a query definition. Use it to type component props that receive query results.
380
+
381
+ ```typescript
382
+ import type { QueryResponseType } from "naystack/graphql";
383
+ import type getCurrentUser from "@/app/api/(graphql)/User/resolvers/get-current-user";
384
+ import type getDeal from "@/app/api/(graphql)/Deal/queries/get-deal";
385
+
386
+ interface DealDetailsProps {
387
+ user: QueryResponseType<typeof getCurrentUser>;
388
+ deal: QueryResponseType<typeof getDeal>;
389
+ }
390
+ ```
391
+
392
+ ### Server-Side Data Fetching
393
+
394
+ #### Direct calls with `.call()` / `.authCall()`
395
+
396
+ Every query definition has `.call()` (unauthenticated or based on `authorized` flag) and `.authCall()` (always reads the refresh cookie for auth). Use these in Server Components.
397
+
398
+ ```typescript
399
+ // In a Server Component:
400
+ const user = await getCurrentUser.authCall();
401
+ const planets = await getPlanets.authCall();
402
+ ```
403
+
404
+ #### `Injector` Component
405
+
406
+ Wraps a client component and injects server-fetched data via Suspense. The component receives `{ data, loading }` as props.
407
+
408
+ ```tsx
409
+ // app/(dashboard)/chat/page.tsx
410
+ import { Injector } from "naystack/graphql/server";
411
+ import getCurrentUser from "@/app/api/(graphql)/User/resolvers/get-current-user";
412
+ import getChats from "@/app/api/(graphql)/Chat/resolvers/get-chats";
413
+ import { ChatWindow } from "./components/chat-window";
414
+
415
+ export default async function ChatPage() {
416
+ return (
417
+ <Injector
418
+ fetch={async () => {
419
+ const user = await getCurrentUser.authCall();
420
+ const chats = await getChats.authCall();
421
+ return { user, chats };
422
+ }}
423
+ Component={ChatWindow}
424
+ />
425
+ );
426
+ }
427
+ ```
104
428
 
105
- Call your GraphQL API from server components or actions:
429
+ The `ChatWindow` component receives `{ data, loading }`:
430
+
431
+ ```tsx
432
+ // components/chat-window.tsx
433
+ export function ChatWindow({ data, loading }: { data?: { user: ...; chats: ... }; loading: boolean }) {
434
+ if (loading) return <Spinner />;
435
+ return <div>{data?.user.name}'s chats: {data?.chats.length}</div>;
436
+ }
437
+ ```
438
+
439
+ #### Server-side `query()` (from `naystack/graphql/server`)
440
+
441
+ Run a raw GraphQL query on the server using the registered Apollo client. Cookies are sent automatically.
106
442
 
107
443
  ```typescript
108
- // gql/server.ts
109
444
  import { query } from "naystack/graphql/server";
110
445
 
111
- // Usage in Page
112
- const data = await query(MyQueryDocument, { variables });
446
+ const data = await query(GetUserDocument, {
447
+ variables: { id: userId },
448
+ revalidate: 60, // Cache for 60s (Next.js ISR)
449
+ tags: ["user"], // For on-demand revalidation
450
+ });
113
451
  ```
114
452
 
115
453
  ### Client Setup (Apollo)
116
454
 
117
- For client-side GraphQL with Apollo:
455
+ Wrap your app with `ApolloWrapper` (inside `AuthWrapper`) so client components can use GraphQL hooks:
118
456
 
119
- ```typescript
120
- // gql/client.ts
457
+ ```tsx
458
+ // app/layout.tsx
459
+ import { AuthWrapper } from "naystack/auth/email/client";
121
460
  import { ApolloWrapper } from "naystack/graphql/client";
122
461
 
462
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
463
+ return (
464
+ <html lang="en">
465
+ <body>
466
+ <AuthWrapper>
467
+ <ApolloWrapper>{children}</ApolloWrapper>
468
+ </AuthWrapper>
469
+ </body>
470
+ </html>
471
+ );
472
+ }
123
473
  ```
124
474
 
125
- ### Frontend Usage
475
+ ### Client Hooks
126
476
 
127
- ```typescript
128
- import { useAuthQuery, useAuthMutation } from "naystack/graphql/client";
477
+ #### `useAuthQuery(query, variables?)`
478
+
479
+ Hook to run a GraphQL query with the current user's token. Returns `[refetch, { data, loading, error }]`.
480
+
481
+ ```tsx
482
+ import { useAuthQuery } from "naystack/graphql/client";
483
+ import { GET_SUMMARY } from "@/constants/graphql/queries";
484
+
485
+ function SummaryCard({ type }: { type: string }) {
486
+ const [getSummary, { loading, data }] = useAuthQuery(GET_SUMMARY);
487
+
488
+ const handleFetch = async () => {
489
+ const result = await getSummary({ type });
490
+ if (result.data?.getSummary) {
491
+ setSummary(result.data.getSummary);
492
+ }
493
+ };
494
+
495
+ return <button onClick={handleFetch} disabled={loading}>Get Summary</button>;
496
+ }
497
+ ```
498
+
499
+ #### `useAuthMutation(mutation, options?)`
129
500
 
130
- function Profile() {
131
- const [getCurrentUser, { loading }] = useAuthMutation(GetCurrentUserDocument);
132
- // ...
501
+ Hook to run a GraphQL mutation with the current user's token. Returns `[mutate, { data, loading, error }]`.
502
+
503
+ ```tsx
504
+ import { useAuthMutation } from "naystack/graphql/client";
505
+ import { CREATE_DEAL } from "@/lib/gql/mutations";
506
+
507
+ function CreateDealModal({ propertyId }: { propertyId: number }) {
508
+ const [createDeal, { loading }] = useAuthMutation(CREATE_DEAL);
509
+
510
+ const onSubmit = async (values: FormFields) => {
511
+ const response = await createDeal({
512
+ propertyId,
513
+ share: Number(values.share),
514
+ targetProfit: Number(values.targetProfit),
515
+ });
516
+ const dealId = response.data?.createDeal;
517
+ if (dealId) router.push(`/deals/${dealId}`);
518
+ };
519
+
520
+ return <form onSubmit={handleSubmit(onSubmit)}>...</form>;
133
521
  }
134
522
  ```
135
523
 
@@ -137,87 +525,254 @@ function Profile() {
137
525
 
138
526
  ## 3. File Upload
139
527
 
140
- Naystack simplifies AWS S3 file uploads with presigned URLs and client-side helpers.
528
+ Naystack simplifies AWS S3 file uploads with presigned URLs and client-side helpers. AWS credentials are read from environment variables automatically.
141
529
 
142
530
  ### Server Setup
143
531
 
144
532
  ```typescript
533
+ // app/api/(rest)/file/route.ts
145
534
  import { setupFileUpload } from "naystack/file";
146
535
 
147
- export const { PUT, uploadFile, getDownloadURL } = setupFileUpload({
148
- region: process.env.AWS_REGION!,
149
- bucket: process.env.AWS_BUCKET!,
150
- awsKey: process.env.AWS_ACCESS_KEY_ID!,
151
- awsSecret: process.env.AWS_SECRET_ACCESS_KEY!,
152
- keys: {
153
- signing: process.env.SIGNING_KEY!,
154
- refresh: process.env.REFRESH_KEY!,
155
- },
536
+ export const { PUT } = setupFileUpload({
537
+ // Called after each successful upload. Return value is sent in the response as `onUploadResponse`.
156
538
  onUpload: async ({ url, type, userId, data }) => {
157
- // Save info to DB or process after successful upload
158
- return { success: true };
539
+ if (type === "DealDocument" && url) {
540
+ const payload = data as { dealId: number; fileName: string; category: string };
541
+ const [row] = await db
542
+ .insert(DealDocumentsTable)
543
+ .values({ dealId: payload.dealId, fileURL: url, fileName: payload.fileName, category: payload.category })
544
+ .returning();
545
+ return row ?? {};
546
+ }
547
+ return {};
159
548
  },
549
+ // Optional: customize the S3 key (defaults to UUID)
550
+ getKey: async ({ type, userId }) => `${type}/${userId}/${crypto.randomUUID()}`,
160
551
  });
161
552
  ```
162
553
 
163
- ### Client Setup & Usage
554
+ The `setupFileUpload` also returns server-side helpers:
164
555
 
165
- ```typescript
166
- import { useFileUpload } from "naystack/file/client";
556
+ - **`uploadFile(keys, { url?, blob? })`** — Upload a file from a URL or Blob to S3.
557
+ - **`deleteFile(url)`** Delete a file by its full S3 URL.
558
+ - **`getUploadURL(keys)`** — Get a presigned PUT URL.
559
+ - **`getDownloadURL(keys)`** — Get the public download URL.
560
+
561
+ ### Client Usage
167
562
 
168
- function UploadButton() {
169
- const upload = useFileUpload();
563
+ ```tsx
564
+ import { useFileUpload } from "naystack/file/client";
170
565
 
171
- const handleFile = async (file: File) => {
172
- const res = await upload(file, "profile-picture", { some: "metadata" });
173
- console.log("Uploaded URL:", res.url);
566
+ function FileUploader({ dealId }: { dealId: number }) {
567
+ const uploadFile = useFileUpload();
568
+ const [uploading, setUploading] = useState(false);
569
+
570
+ const handleUpload = async (file: File) => {
571
+ setUploading(true);
572
+ try {
573
+ const result = await uploadFile(file, "DealDocument", {
574
+ dealId,
575
+ fileName: file.name,
576
+ category: "Contract",
577
+ });
578
+ if (result?.url) {
579
+ console.log("Uploaded:", result.url);
580
+ router.refresh();
581
+ }
582
+ } finally {
583
+ setUploading(false);
584
+ }
174
585
  };
175
- // ...
586
+
587
+ return <input type="file" onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])} />;
176
588
  }
177
589
  ```
178
590
 
179
591
  ---
180
592
 
181
- ## 4. Other Utilities
182
-
183
- ### Client Hooks
184
-
185
- - `useVisibility(onVisible)`: Triggers a callback when an element enters the viewport.
186
- - `useBreakpoint(query)`: Responsive media query hook.
593
+ ## 4. Client Utilities
187
594
 
188
595
  ### SEO
189
596
 
190
- The `setupSEO` utility helps generate optimized Next.js metadata.
597
+ The `setupSEO` utility creates a metadata factory for Next.js. Call it once with your site defaults, then use the returned function per-page.
191
598
 
192
599
  ```typescript
600
+ // lib/utils/seo.ts
193
601
  import { setupSEO } from "naystack/client";
194
602
 
195
- export const getMetadata = setupSEO({
196
- siteName: "Naystack",
197
- title: "A powerful stack",
198
- description: "Built with Next.js",
199
- themeColor: "#000000",
603
+ export const getSEO = setupSEO({
604
+ title: "My App - Tagline",
605
+ description: "Description of my application.",
606
+ siteName: "My App",
607
+ themeColor: "#5b9364",
200
608
  });
609
+
610
+ // In a page:
611
+ export const metadata = getSEO("Dashboard", "Your personalized dashboard");
612
+ // Produces: title = "Dashboard • My App", description = "Your personalized dashboard"
613
+ ```
614
+
615
+ ### `useVisibility(onVisible?)`
616
+
617
+ Triggers a callback when a DOM element enters the viewport. Returns a ref to attach to the observed element.
618
+
619
+ ```tsx
620
+ import { useVisibility } from "naystack/client";
621
+
622
+ function LazySection() {
623
+ const ref = useVisibility(() => loadMoreData());
624
+ return <section ref={ref}>...</section>;
625
+ }
626
+ ```
627
+
628
+ ### `useBreakpoint(query)`
629
+
630
+ Responsive media query hook. Returns `true`/`false` or `null` during SSR.
631
+
632
+ ```tsx
633
+ import { useBreakpoint } from "naystack/client";
634
+
635
+ function ResponsiveNav() {
636
+ const isMobile = useBreakpoint("(max-width: 639px)");
637
+ if (isMobile === null) return <Skeleton />;
638
+ return isMobile ? <MobileNav /> : <DesktopNav />;
639
+ }
201
640
  ```
202
641
 
203
- ### Social APIs
642
+ ---
643
+
644
+ ## 5. Social APIs
645
+
646
+ Simplified access to Instagram Graph API and Threads API.
647
+
648
+ ### Instagram
649
+
650
+ ```typescript
651
+ import {
652
+ getInstagramUser,
653
+ getInstagramMedia,
654
+ getInstagramConversations,
655
+ getInstagramConversation,
656
+ getInstagramMessage,
657
+ sendInstagramMessage,
658
+ setupInstagramWebhook,
659
+ } from "naystack/socials";
660
+
661
+ // Fetch the authenticated user's profile
662
+ const user = await getInstagramUser(accessToken);
663
+ // => { username: "johndoe", followers_count: 1234, media_count: 56 }
664
+
665
+ // Fetch recent media
666
+ const media = await getInstagramMedia(accessToken, undefined, 10);
667
+ // => { data: [{ like_count: 5, comments_count: 2, permalink: "..." }, ...] }
668
+
669
+ // Fetch conversations with pagination
670
+ const convos = await getInstagramConversations(accessToken, 25);
671
+ for (const convo of convos.data ?? []) {
672
+ console.log(convo.participants, convo.messages);
673
+ }
674
+ if (convos.fetchMore) {
675
+ const nextPage = await convos.fetchMore();
676
+ }
677
+
678
+ // Send a message
679
+ await sendInstagramMessage(accessToken, recipientId, "Hello!");
680
+
681
+ // Webhook setup (app/api/webhooks/instagram/route.ts)
682
+ export const { GET, POST } = setupInstagramWebhook({
683
+ secret: process.env.WEBHOOK_SECRET!,
684
+ callback: async (type, value, id) => {
685
+ console.log("Webhook event:", type, value, id);
686
+ },
687
+ });
688
+ ```
204
689
 
205
- Simplified access to Instagram and Threads APIs.
690
+ ### Threads
206
691
 
207
692
  ```typescript
208
- import { getInstagramUser, createThreadsPost } from "naystack/socials";
693
+ import {
694
+ getThread,
695
+ getThreads,
696
+ getThreadsReplies,
697
+ createThreadsPost,
698
+ createThread,
699
+ setupThreadsWebhook,
700
+ } from "naystack/socials";
701
+
702
+ // Fetch user's threads
703
+ const threads = await getThreads(accessToken);
704
+ // => [{ text: "Hello world", permalink: "...", username: "johndoe" }]
705
+
706
+ // Create and publish a single post
707
+ const postId = await createThreadsPost(accessToken, "Hello from Naystack!");
708
+
709
+ // Create a thread (sequence of posts)
710
+ const firstPostId = await createThread(accessToken, [
711
+ "First post in thread",
712
+ "Second post (reply to first)",
713
+ "Third post (reply to second)",
714
+ ]);
715
+
716
+ // Webhook setup (app/api/webhooks/threads/route.ts)
717
+ export const { GET, POST } = setupThreadsWebhook({
718
+ secret: process.env.WEBHOOK_SECRET!,
719
+ callback: async (field, value) => {
720
+ console.log("Threads event:", field, value);
721
+ return true; // Return false to respond with 500
722
+ },
723
+ });
209
724
  ```
210
725
 
211
726
  ---
212
727
 
213
- ## Minimal Environment Variables
728
+ ## Environment Variables
729
+
730
+ Naystack reads configuration from environment variables. Set the ones you need based on which modules you use.
214
731
 
215
- ```env
732
+ ### Required (Core Auth)
733
+
734
+ ```bash
216
735
  SIGNING_KEY=your-jwt-signing-key
217
736
  REFRESH_KEY=your-jwt-refresh-key
218
- NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:3000/api
737
+ ```
738
+
739
+ ### Required (Client-side endpoints)
740
+
741
+ ```bash
742
+ NEXT_PUBLIC_EMAIL_AUTH_ENDPOINT=/api/email
743
+ NEXT_PUBLIC_GRAPHQL_ENDPOINT=/api/graphql
744
+ NEXT_PUBLIC_FILE_ENDPOINT=/api/file
745
+ NEXT_PUBLIC_BASE_URL=https://yourapp.com
746
+ ```
747
+
748
+ ### Google OAuth
749
+
750
+ ```bash
751
+ GOOGLE_CLIENT_ID=your-google-client-id
752
+ GOOGLE_CLIENT_SECRET=your-google-client-secret
753
+ NEXT_PUBLIC_GOOGLE_AUTH_ENDPOINT=/api/google
754
+ ```
755
+
756
+ ### Instagram OAuth
757
+
758
+ ```bash
759
+ INSTAGRAM_CLIENT_ID=your-instagram-client-id
760
+ INSTAGRAM_CLIENT_SECRET=your-instagram-client-secret
761
+ NEXT_PUBLIC_INSTAGRAM_AUTH_ENDPOINT=/api/instagram
762
+ ```
763
+
764
+ ### AWS S3 (File Upload)
765
+
766
+ ```bash
219
767
  AWS_REGION=us-east-1
220
768
  AWS_BUCKET=your-bucket-name
221
- AWS_ACCESS_KEY_ID=xxx
222
- AWS_SECRET_ACCESS_KEY=xxx
769
+ AWS_ACCESS_KEY_ID=your-access-key-id
770
+ AWS_ACCESS_KEY_SECRET=your-secret-access-key
771
+ ```
772
+
773
+ ### Optional
774
+
775
+ ```bash
776
+ TURNSTILE_KEY=cloudflare-turnstile-secret-key
777
+ NODE_ENV=production
223
778
  ```