Package Exports
- naystack/auth
- naystack/socials
Readme
Naystack
A stack built with tight Next.js + Drizzle ORM + GraphQL integration
Installation
npm install naystack
# or
pnpm add naystack
# or
yarn add naystackModules
Naystack provides the following modules, each accessible via its own import path:
| Module | Import Path | Description |
|---|---|---|
| Auth | naystack/auth |
Email, Google, and Instagram authentication |
| GraphQL | naystack/graphql |
GraphQL server initialization with type-graphql |
| Client | naystack/client |
Client-side hooks and utilities |
| File | naystack/file |
File upload to AWS S3 |
| Socials | naystack/socials |
Instagram and Threads API integration |
Auth Module
import {
getEmailAuthRoutes,
initGoogleAuth,
initInstagramAuth,
} from "naystack/auth";Email Authentication
Setup email-based authentication with JWT tokens and optional Turnstile captcha verification.
Basic Example:
const emailAuth = getEmailAuthRoutes({
getUser: async (email: string) => { /* fetch user by email */ },
createUser: async (user: UserInput) => { /* create new user */ },
signingKey: process.env.JWT_SIGNING_KEY!,
refreshKey: process.env.JWT_REFRESH_KEY!,
turnstileKey?: string, // Optional: Cloudflare Turnstile secret key
onSignUp: (user) => { /* callback on signup */ },
onLogout?: (body) => { /* callback on logout */ },
onError?: (error) => { /* custom error handler */ },
});
// Export in Next.js route handler
export const { GET, POST, PUT, DELETE, getUserIdFromRequest } = emailAuth;Real-World Example with Drizzle ORM:
import { db } from "@/lib/db";
import { UserTable, WebPushSubscriptionTable } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { getEmailAuthRoutes } from "naystack/auth";
import { waitUntil } from "@vercel/functions";
export const { GET, POST, PUT, DELETE, getUserIdFromRequest } =
getEmailAuthRoutes({
// Fetch user by email using Drizzle
getUser: (email: string) =>
db.query.UserTable.findFirst({
where: eq(UserTable.email, email)
}),
// Create new user
createUser: async (user) => {
const [newUser] = await db
.insert(UserTable)
.values(user)
.returning();
return newUser;
},
signingKey: process.env.SIGNING_KEY!,
refreshKey: process.env.REFRESH_KEY!,
turnstileKey: process.env.TURNSTILE_KEY!,
// Send welcome email on signup
onSignUp: ({ id, email }) =>
waitUntil(
(async () => {
const link = await getVerificationLink(id);
if (link && email) {
await sendTemplateEmail(email, "WelcomeUser", {
verifyLink: link,
});
}
})(),
),
// Clean up push subscriptions on logout
onLogout: async (endpoint: string) => {
await db
.delete(WebPushSubscriptionTable)
.where(eq(WebPushSubscriptionTable.endpoint, endpoint));
},
});Options
| Option | Type | Required | Description |
|---|---|---|---|
getUser |
(email: string) => Promise<UserOutput | undefined> |
✅ | Fetch user by email |
createUser |
(user: UserInput) => Promise<UserOutput | undefined> |
✅ | Create new user |
signingKey |
string |
✅ | JWT signing key |
refreshKey |
string |
✅ | JWT refresh key |
turnstileKey |
string |
❌ | Cloudflare Turnstile secret key |
onSignUp |
(user: UserOutput) => void |
✅ | Callback when user signs up |
onLogout |
(body: string) => Promise<void> |
❌ | Callback on logout |
onError |
ErrorHandler |
❌ | Custom error handler |
Types
interface UserInput {
email: string;
password: string;
[key: string]: unknown;
}
interface UserOutput {
id: number;
email: string;
password: string | null;
[key: string]: unknown;
}Google Authentication
Real-World Example:
import { db } from "@/lib/db";
import { UserTable } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { initGoogleAuth } from "naystack/auth";
export const { GET } = initGoogleAuth({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authRoute: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/google`,
successRedirectURL: "/dashboard",
errorRedirectURL: "/login?error=google",
refreshKey: process.env.REFRESH_KEY!,
// Find existing user or create new one
getUserIdFromEmail: async ({ email, name }) => {
if (!email) return null;
// Update existing user's email verification status
const [existingUser] = await db
.update(UserTable)
.set({ emailVerified: true })
.where(eq(UserTable.email, email))
.returning({ id: UserTable.id });
if (existingUser) {
return existingUser.id;
}
// Create new user if doesn't exist
if (name && email) {
const [newUser] = await db
.insert(UserTable)
.values({
email,
name,
emailVerified: true,
})
.returning({ id: UserTable.id });
return newUser?.id || null;
}
return null;
},
});Options
| Option | Type | Required | Description |
|---|---|---|---|
clientId |
string |
✅ | Google OAuth client ID |
clientSecret |
string |
✅ | Google OAuth client secret |
authRoute |
string |
✅ | OAuth callback route |
successRedirectURL |
string |
✅ | Redirect URL on success |
errorRedirectURL |
string |
✅ | Redirect URL on error |
refreshKey |
string |
✅ | JWT refresh key |
getUserIdFromEmail |
(email: Schema$Userinfo) => Promise<number | null> |
✅ | Get user ID from Google user info |
Instagram Authentication
Real-World Example:
import { db } from "@/lib/db";
import { InstagramDetails, UserTable } from "@/lib/db/schema";
import { and, eq, ne } from "drizzle-orm";
import { initInstagramAuth } from "naystack/auth";
export const { GET, getRefreshedAccessToken } = initInstagramAuth({
clientId: process.env.NEXT_PUBLIC_INSTAGRAM_CLIENT_ID!,
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET!,
authRoute: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/instagram`,
successRedirectURL: "/profile",
errorRedirectURL: "/signup",
refreshKey: process.env.REFRESH_KEY!,
// Verify and link Instagram account
onUser: async (instagramData, userId, accessToken) => {
if (!userId) return "You are not logged in";
const user = await db.query.UserTable.findFirst({
where: eq(UserTable.id, userId),
});
if (!user?.instagramDetails) {
return "Please connect Instagram first";
}
// Update Instagram verification status
const [updated] = await db
.update(InstagramDetails)
.set({
isVerified: true,
accessToken: accessToken
})
.where(
and(
eq(InstagramDetails.id, user.instagramDetails),
eq(InstagramDetails.username, instagramData.username),
),
)
.returning({ username: InstagramDetails.username });
if (!updated) {
return "Please login with the same username as the connected account";
}
// Disconnect from other users if linked
await db
.update(UserTable)
.set({ instagramDetails: null })
.where(
and(
ne(UserTable.id, userId),
eq(UserTable.instagramDetails, user.instagramDetails),
),
);
},
});Options
| Option | Type | Required | Description |
|---|---|---|---|
clientId |
string |
✅ | Instagram app client ID |
clientSecret |
string |
✅ | Instagram app client secret |
authRoute |
string |
✅ | OAuth callback route |
successRedirectURL |
string |
✅ | Redirect URL on success |
errorRedirectURL |
string |
✅ | Redirect URL on error |
refreshKey |
string |
✅ | JWT refresh key |
onUser |
(data: InstagramUser, id: number | null, accessToken: string) => Promise<string | void> |
✅ | Callback with Instagram user data |
GraphQL Module
import {
initGraphQLServer,
GQLError,
query,
field,
QueryLibrary,
FieldLibrary,
} from "naystack/graphql";
import type { Context, AuthorizedContext } from "naystack/graphql";Initialize GraphQL Server
Basic Example:
const { GET, POST } = await initGraphQLServer({
resolvers: [UserResolver, PostResolver],
authChecker: ({ context }) => !!context.userId,
plugins: [], // Optional Apollo plugins
context: async (req) => ({
// Custom context builder
userId: await getUserIdFromRequest(req),
}),
});
export { GET, POST };Real-World Example with Advanced Context:
import { initGraphQLServer } from "naystack/graphql";
import { getUserIdFromRequest } from "@/api/(auth)/email/setup";
import { authChecker } from "@/lib/auth/context";
export const { GET, POST } = await initGraphQLServer({
resolvers: [
UserResolver,
PostResolver,
ApplicationResolver,
ChatResolver,
// ... more resolvers
],
authChecker,
context: async (req) => {
const res = getUserIdFromRequest(req);
if (!res) return { userId: null };
// Handle refresh token user ID
if (res.refreshUserID) {
const isMobile = req.headers.get("x-platform-is-mobile");
if (isMobile) return { userId: null };
return { userId: res.refreshUserID, onlyQuery: true };
}
// Handle access token user ID
if (res.accessUserId) {
return { userId: res.accessUserId };
}
return { userId: null };
},
});Options
| Option | Type | Required | Description |
|---|---|---|---|
resolvers |
NonEmptyArray<Function> |
✅ | Array of TypeGraphQL resolvers |
authChecker |
AuthChecker<any> |
❌ | Custom auth checker function |
plugins |
ApolloServerPlugin[] |
❌ | Additional Apollo Server plugins |
context |
(req: NextRequest) => Promise<any> |
❌ | Context builder function |
Error Handling
Basic Usage:
import { GQLError } from "naystack/graphql";
// Usage in resolvers
throw GQLError(404, "User not found");
throw GQLError(403); // "You are not allowed to perform this action"
throw GQLError(400); // "Please provide all required inputs"
throw GQLError(); // "Server Error" (500)Real-World Example in Resolvers:
import { GQLError } from "naystack/graphql";
import { db } from "@/lib/db";
import { UserTable, PostingTable } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
export async function createPosting(
ctx: Context,
input: NewPostingInput,
): Promise<number | null> {
// Authentication check
if (!ctx.userId) {
throw GQLError(400, "Please login to create posting");
}
const user = await db.query.UserTable.findFirst({
where: eq(UserTable.id, ctx.userId),
});
// Authorization check
if (!user || user.role === "CREATOR") {
throw GQLError(400, "Only onboarded users can create postings");
}
// Validation check
if (!user.emailVerified) {
throw GQLError(400, "Please verify email to create posting");
}
// Rate limiting check
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const recentPostings = await db.query.PostingTable.findMany({
where: and(
eq(PostingTable.userId, ctx.userId),
gte(PostingTable.createdAt, yesterday),
),
});
if (recentPostings.length >= MAXIMUM_POSTINGS_DAY) {
throw GQLError(
400,
`Only ${MAXIMUM_POSTINGS_DAY} allowed in 24 hours. Try again later.`,
);
}
// Create posting...
}Query & Field Helpers
Build resolvers functionally using query, field, QueryLibrary, and FieldLibrary:
Basic Example:
import { query, QueryLibrary, field, FieldLibrary } from "naystack/graphql";
// Define queries/mutations
const queries = {
getUser: query(
async (ctx, input) => {
return await db.query.users.findFirst({ where: eq(users.id, input) });
},
{
output: User,
input: Number,
authorized: true,
}
),
createUser: query(
async (ctx, input) => {
/* ... */
},
{
output: User,
input: CreateUserInput,
mutation: true, // Makes this a mutation instead of query
}
),
};
// Generate resolver class
const UserResolver = QueryLibrary(queries);
// Define field resolvers
const fields = {
posts: field(
async (root, ctx) => {
return await db.query.posts.findMany({
where: eq(posts.userId, root.id),
});
},
{ output: [Post] }
),
};
const UserFieldResolver = FieldLibrary(User, fields);Real-World Example:
import { query, QueryLibrary, field, FieldLibrary } from "naystack/graphql";
import { db } from "@/lib/db";
import { NotificationTable } from "@/lib/db/schema";
import { eq, lte } from "drizzle-orm";
import { waitUntil } from "@vercel/functions";
// Query with side effects
export const getNotifications = query(
async (ctx) => {
if (!ctx.userId) return [];
// Mark all as read
const notifications = await db
.update(NotificationTable)
.set({ read: true })
.where(eq(NotificationTable.user, ctx.userId))
.returning();
// Clean up old notifications (async)
const weekBefore = new Date();
weekBefore.setDate(weekBefore.getDate() - 7);
waitUntil(
db
.delete(NotificationTable)
.where(lte(NotificationTable.createdAt, weekBefore)),
);
return notifications.sort((a, b) => a.id - b.id);
},
{
output: [NotificationGQL!],
},
);
// Create resolver from queries
export const NotificationResolver = QueryLibrary({
getNotifications,
getUnreadNotifications,
});
// Field resolver example
export const UserFields = FieldLibrary(UserGQL, {
isOnboarded: field(
async (user) => {
return getIsOnboarded(user);
},
{
output: Boolean,
}
),
});Types
interface Context {
userId: number | null;
}
interface AuthorizedContext {
userId: number;
}Client Module
import {
useVisibility,
useBreakpoint,
setupSEO,
getHandleImageUpload,
getInstagramAuthorizationURLSetup,
} from "naystack/client";Hooks
useVisibility
Observe element visibility using Intersection Observer. Perfect for infinite scroll and lazy loading.
Basic Example:
function Component() {
const ref = useVisibility(() => {
console.log("Element is visible!");
});
return <div ref={ref}>Watch me!</div>;
}Real-World Example - Infinite Scroll:
"use client";
import { useVisibility } from "naystack/client";
import { useRef } from "react";
export default function PostingCard({
posting,
fetchMore
}: {
posting: Posting;
fetchMore?: () => void;
}) {
// Trigger fetchMore when card becomes visible
const mainRef = useVisibility(fetchMore);
return (
<div ref={mainRef} className="posting-card">
<h3>{posting.title}</h3>
<p>{posting.description}</p>
</div>
);
}useBreakpoint
React to media query changes. Useful for responsive layouts and conditional rendering.
Basic Example:
function Component() {
const isMobile = useBreakpoint("(max-width: 768px)");
return <div>{isMobile ? "Mobile" : "Desktop"}</div>;
}Real-World Example - Responsive Layout:
"use client";
import { useBreakpoint } from "naystack/client";
export default function LayoutWrapper({
children
}: {
children: React.ReactNode;
}) {
const isLarge = useBreakpoint("(min-width: 1024px)");
return (
<div
style={{
height: isLarge
? "calc(100svh - 80px)"
: "calc(100svh - 55px)",
}}
className="flex flex-col"
>
{children}
</div>
);
}SEO Helper
const getSEO = setupSEO({
title: "My App",
description: "Default description",
siteName: "MyApp",
themeColor: "#000000",
});
// In page
export const metadata = getSEO(
"Page Title",
"Page description",
"/og-image.png"
);Instagram Authorization URL
Generate Instagram OAuth authorization URLs for client-side redirects.
Example:
import { getInstagramAuthorizationURLSetup } from "naystack/client";
// Setup once
const getInstagramAuthorizationURL = getInstagramAuthorizationURLSetup(
process.env.NEXT_PUBLIC_INSTAGRAM_CLIENT_ID!,
`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/instagram`,
);
// Usage in component
function ConnectInstagramButton() {
const handleConnect = () => {
const authURL = getInstagramAuthorizationURL(userToken);
window.location.href = authURL;
};
return <button onClick={handleConnect}>Connect Instagram</button>;
}Image Upload Client
const uploadImage = getHandleImageUpload("/api/upload");
const result = await uploadImage({
file: imageFile,
token: authToken,
type: "avatar",
data: { userId: 123 }, // Optional additional data
sync: true, // Optional: wait for processing
});File Module
import { setupFileUpload } from "naystack/file";Setup File Upload
Basic Example:
const fileUpload = setupFileUpload({
refreshKey: process.env.JWT_REFRESH_KEY!,
signingKey: process.env.JWT_SIGNING_KEY!,
region: "us-east-1",
bucket: "my-bucket",
awsKey: process.env.AWS_ACCESS_KEY_ID!,
awsSecret: process.env.AWS_SECRET_ACCESS_KEY!,
processFile: async ({ url, type, userId, data }) => {
// Process uploaded file
return {
deleteURL: url, // URL to delete if needed
response: { success: true },
};
},
});
// Export route handler
export const { PUT } = fileUpload;
// Server-side utilities
const { getUploadFileURL, uploadImage, deleteImage, getFileURL, uploadFile } =
fileUpload;Real-World Example with Multiple File Types:
import { setupFileUpload } from "naystack/file";
import { db } from "@/lib/db";
import { PortfolioTable, UserTable } from "@/lib/db/schema";
import { and, eq } from "drizzle-orm";
import { waitUntil } from "@vercel/functions";
export const { deleteImage, getUploadFileURL, getFileURL, uploadImage, PUT } =
setupFileUpload({
region: process.env.SITE_AWS_REGION!,
refreshKey: process.env.REFRESH_KEY!,
awsSecret: process.env.SITE_AWS_SECRET_ACCESS_KEY!,
awsKey: process.env.SITE_AWS_ACCESS_KEY_ID!,
signingKey: process.env.SIGNING_KEY!,
bucket: process.env.SITE_AWS_BUCKET!,
processFile: async ({ url, userId, data, type }) => {
switch (type) {
case "PORTFOLIO":
// Handle portfolio image upload
waitUntil(
(async () => {
if (!url) return {};
const id = (data as { id?: number }).id;
if (id) {
// Update existing portfolio
const [existing] = await db
.select()
.from(PortfolioTable)
.where(
and(
eq(PortfolioTable.id, id),
eq(PortfolioTable.user, userId),
),
);
if (!existing) {
return { deleteURL: url };
}
const oldURL = existing.imageURL;
await db
.update(PortfolioTable)
.set({ imageURL: url })
.where(
and(
eq(PortfolioTable.id, id),
eq(PortfolioTable.user, userId),
),
);
return {
deleteURL: oldURL,
data: { id },
};
} else {
// Create new portfolio
const [portfolio] = await db
.insert(PortfolioTable)
.values({
user: userId,
imageURL: url,
caption: "",
link: "",
})
.returning({ id: PortfolioTable.id });
return { data: { id: portfolio?.id } };
}
})(),
);
break;
case "PROFILE_PICTURE":
// Handle profile picture upload
waitUntil(
(async () => {
const user = await db.query.UserTable.findFirst({
where: eq(UserTable.id, userId),
});
if (!user && url) {
return { deleteURL: url };
}
const oldPhoto = user?.photo;
await db
.update(UserTable)
.set({ photo: url })
.where(eq(UserTable.id, userId));
return {
deleteURL: oldPhoto || undefined,
};
})(),
);
break;
}
return {};
},
});Options
| Option | Type | Required | Description |
|---|---|---|---|
refreshKey |
string |
✅ | JWT refresh key |
signingKey |
string |
✅ | JWT signing key |
region |
string |
✅ | AWS S3 region |
bucket |
string |
✅ | AWS S3 bucket name |
awsKey |
string |
✅ | AWS access key ID |
awsSecret |
string |
✅ | AWS secret access key |
processFile |
Function |
✅ | File processing callback |
Returned Utilities
| Utility | Description |
|---|---|
PUT |
Route handler for file uploads |
getUploadFileURL |
Get presigned URL for upload |
uploadImage |
Upload image to S3 |
deleteImage |
Delete image from S3 |
getFileURL |
Get public URL for a file |
uploadFile |
Upload any file to S3 |
Socials Module
import {
// Instagram
getInstagramUser,
getInstagramMedia,
getInstagramConversations,
getInstagramConversationsByUser,
getInstagramConversationByUser,
getInstagramConversation,
getInstagramMessage,
sendInstagramMessage,
setupInstagramWebhook,
// Threads
getThread,
getThreads,
getThreadsReplies,
createThread,
createThreadsPost,
setupThreadsWebhook,
} from "naystack/socials";Instagram API
Get User Data
const user = await getInstagramUser(accessToken);
const user = await getInstagramUser(accessToken, "user_id");
const user = await getInstagramUser(accessToken, "me", [
"username",
"followers_count",
]);Get Media
Basic Usage:
const media = await getInstagramMedia(accessToken);
const media = await getInstagramMedia(
accessToken,
["like_count", "comments_count"],
24
);Real-World Example - Fetching Media with Custom Fields:
import { getInstagramMedia } from "naystack/socials";
export async function fetchInstagramGraphMedia(
accessToken: string,
followers: number,
userId: number,
) {
const fetchReq = await getInstagramMedia<{
thumbnail_url?: string;
id: string;
like_count?: number;
comments_count: number;
permalink: string;
caption: string;
media_url?: string;
media_type?: string;
timestamp: string;
}>(accessToken, [
"id",
"thumbnail_url",
"media_url",
"like_count",
"comments_count",
"media_type",
"permalink",
"caption",
"timestamp",
]);
if (fetchReq?.data) {
return fetchReq.data.map((media) => ({
isVideo: media.media_type === "VIDEO",
comments: media.comments_count || -1,
likes: media.like_count || 0,
link: media.permalink,
thumbnail: media.thumbnail_url || media.media_url,
mediaURL: media.media_url,
timestamp: media.timestamp,
caption: media.caption,
appID: media.id,
user: userId,
er: calculateEngagementRate(
followers,
media.like_count || 0,
media.comments_count || -1,
),
}));
}
}Conversations
// Get all conversations
const { data, fetchMore } = await getInstagramConversations(accessToken);
// Get conversations by user
const conversations = await getInstagramConversationsByUser(
accessToken,
userId
);
// Get single conversation
const conversation = await getInstagramConversationByUser(accessToken, userId);
// Get conversation with messages
const { messages, participants, fetchMore } = await getInstagramConversation(
accessToken,
conversationId
);Messages
// Get message details
const message = await getInstagramMessage(accessToken, messageId);
// Send message
const result = await sendInstagramMessage(accessToken, recipientId, "Hello!");Webhook
Basic Example:
const instagramWebhook = setupInstagramWebhook({
secret: process.env.INSTAGRAM_WEBHOOK_SECRET!,
callback: async (type, value, id) => {
// Handle webhook events
},
});
export const { GET, POST } = instagramWebhook;Real-World Example - Auto-Reply Bot:
import {
getInstagramConversationByUser,
sendInstagramMessage,
setupInstagramWebhook,
} from "naystack/socials";
export const { GET, POST } = setupInstagramWebhook({
secret: process.env.REFRESH_KEY!,
callback: async (
type,
value: {
sender: { id: string };
message: { text: string };
recipient: { id: string };
},
) => {
if (
type === "messaging" &&
value.message.text &&
value.sender.id !== "YOUR_PAGE_ID" &&
value.recipient.id === "YOUR_PAGE_ID"
) {
// Check if message is recent (within 24 hours)
const conversation = await getInstagramConversationByUser(
process.env.INSTAGRAM_ACCESS_TOKEN!,
value.sender.id,
);
const lastMessage = conversation?.messages?.data[1]?.created_time;
if (lastMessage) {
const lastMessageDate = new Date(lastMessage);
if (lastMessageDate.getTime() > Date.now() - 1000 * 60 * 60 * 24) {
return; // Already replied recently
}
}
// Generate reply (using your AI/LLM service)
const reply = await generateReply(value.message.text);
if (reply && reply !== '""') {
await sendInstagramMessage(
process.env.INSTAGRAM_ACCESS_TOKEN!,
value.sender.id,
reply,
);
}
}
},
});Threads API
Get Threads
// Get single thread
const thread = await getThread(accessToken, threadId);
const thread = await getThread(accessToken, threadId, ["text", "permalink"]);
// Get all threads
const threads = await getThreads(accessToken);
// Get thread replies
const replies = await getThreadsReplies(accessToken, threadId);Create Threads
// Create single post
const postId = await createThreadsPost(accessToken, "Hello, Threads!");
// Reply to a thread
const replyId = await createThreadsPost(
accessToken,
"Reply text",
parentThreadId
);
// Create threaded posts (carousel)
const firstPostId = await createThread(accessToken, [
"First post",
"Second post in thread",
"Third post in thread",
]);Webhook
Basic Example:
const threadsWebhook = setupThreadsWebhook({
secret: process.env.THREADS_WEBHOOK_SECRET!,
callback: async (type, value) => {
// Handle webhook events
return true; // Return false to indicate failure
},
});
export const { GET, POST } = threadsWebhook;Real-World Example - Auto-Reply to Threads:
import {
createThreadsPost,
getThreadsReplies,
setupThreadsWebhook,
} from "naystack/socials";
import { db } from "@/lib/db";
import { SocialPostsTable } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
const REPLIES = [
"Thanks for your interest! Check out campaign ${ID}",
"We'd love to work with you! See campaign ${ID}",
];
export const { GET, POST } = setupThreadsWebhook({
secret: process.env.REFRESH_KEY!,
callback: async (
field,
value: {
id: string;
username: string;
text: string;
replied_to: { id: string };
root_post: { id: string; username: string };
},
) => {
// Skip if replying to own post
if (value.root_post.username === value.username) return true;
// Only reply to direct replies to root post
if (value.root_post.id !== value.replied_to.id) return true;
// Check if already replied
const replies = await getThreadsReplies(
process.env.THREADS_ACCESS_TOKEN!,
value.id,
);
if (replies?.length ?? 0 > 0) return true;
// Find associated campaign
const [post] = await db
.select()
.from(SocialPostsTable)
.where(eq(SocialPostsTable.postID, value.root_post.id));
if (!post) return true;
// Generate reply message
const message = REPLIES[
Math.floor(Math.random() * REPLIES.length)
]?.replace("${ID}", post.campaignID.toString());
if (!message) return true;
// Send reply
const res = await createThreadsPost(
process.env.THREADS_ACCESS_TOKEN!,
message,
value.id,
);
return !!res;
},
});Best Practices & Common Patterns
Authentication Flow
Email Auth with Database Integration:
- Use Drizzle ORM queries in
getUserandcreateUser - Send verification emails in
onSignUpcallback - Clean up session data in
onLogout
- Use Drizzle ORM queries in
OAuth Integration:
- Always verify email/username matches existing accounts
- Create new users only when necessary
- Update verification status for existing users
GraphQL Resolver Patterns
Error Handling:
- Use
GQLErrorfor all error cases - Provide clear, user-friendly error messages
- Use appropriate HTTP status codes (400, 403, 404, 500)
- Use
Authorization:
- Check authentication first (
if (!ctx.userId)) - Verify permissions before operations
- Use
authCheckerfor role-based access control
- Check authentication first (
Query Library Pattern:
- Use
query()helper for simple queries/mutations - Use
QueryLibrary()to combine multiple queries - Use
field()andFieldLibrary()for computed fields
- Use
File Upload Patterns
Multiple File Types:
- Use
typeparameter to distinguish upload types - Handle each type in
processFilecallback - Return
deleteURLfor old files to clean up
- Use
Async Processing:
- Use
waitUntil()for non-blocking operations - Process file metadata in background
- Update database after successful upload
- Use
Webhook Patterns
Instagram Webhooks:
- Verify sender/recipient IDs
- Check message timestamps to avoid duplicates
- Use async operations for replies
Threads Webhooks:
- Filter by reply structure (root_post vs replied_to)
- Check for existing replies before responding
- Return
true/falseto indicate success/failure
Client-Side Patterns
Infinite Scroll:
- Use
useVisibilityhook withfetchMorecallback - Attach ref to last item in list
- Handle loading states
- Use
Responsive Design:
- Use
useBreakpointfor conditional rendering - Adjust layout based on screen size
- Combine with CSS for optimal UX
- Use
Environment Variables
Here's a summary of common environment variables used:
# JWT Keys
JWT_SIGNING_KEY=your-signing-key
JWT_REFRESH_KEY=your-refresh-key
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Instagram
INSTAGRAM_CLIENT_ID=your-instagram-client-id
INSTAGRAM_CLIENT_SECRET=your-instagram-client-secret
INSTAGRAM_WEBHOOK_SECRET=your-instagram-webhook-secret
# Threads
THREADS_WEBHOOK_SECRET=your-threads-webhook-secret
# AWS S3
AWS_ACCESS_KEY_ID=your-aws-key
AWS_SECRET_ACCESS_KEY=your-aws-secret
# Cloudflare Turnstile (optional)
TURNSTILE_SECRET_KEY=your-turnstile-key
# App
NEXT_PUBLIC_BASE_URL=https://your-app.comLicense
ISC © Abhinay Pandey