naystack - v1.5.10
    Preparing search index...

    naystack - v1.5.10

    Naystack

    A minimal, powerful stack for Next.js app development. Built with Next.js + Drizzle ORM + GraphQL + S3 + Auth.

    npm version License: ISC

    API Reference

    pnpm add naystack
    

    Naystack provides a seamless email-based authentication system with optional support for Google and Instagram OAuth.

    Define your auth routes in app/api/(auth)/email/route.ts. The library reads SIGNING_KEY and REFRESH_KEY from environment variables automatically.

    import { getEmailAuthRoutes } from "naystack/auth";
    import { db } from "@/app/api/lib/db";
    import { UserTable } from "@/app/api/(graphql)/User/db";
    import { eq } from "drizzle-orm";

    export const { GET, POST, PUT, DELETE } = getEmailAuthRoutes({
    // Fetch user by request data (used for login & sign-up duplicate check)
    getUser: async ({ email }: { email: string }) => {
    const [user] = await db
    .select({ id: UserTable.id, password: UserTable.password })
    .from(UserTable)
    .where(eq(UserTable.email, email));
    return user;
    },
    // Create a new user with the hashed password
    createUser: async (data: { email: string; password: string; name: string }) => {
    const [user] = await db
    .insert(UserTable)
    .values(data)
    .returning({ id: UserTable.id, password: UserTable.password });
    return user;
    },
    // Optional: callback after successful sign-up
    onSignUp: async (userId, body: { orgTitle?: string }) => {
    if (body.orgTitle && userId) {
    await createOrg(userId, { title: body.orgTitle });
    }
    },
    });

    The returned route handlers map to:

    Handler HTTP Method Purpose
    GET GET Refresh tokens (exchange refresh cookie)
    POST POST Sign up (create user, return tokens)
    PUT PUT Login (verify credentials, return tokens)
    DELETE DELETE Logout (clear refresh cookie)

    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.

    // app/layout.tsx
    import { AuthWrapper } from "naystack/auth/client";
    import { ApolloWrapper } from "naystack/graphql/client";

    export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
    <html lang="en">
    <body>
    <AuthWrapper>
    <ApolloWrapper>{children}</ApolloWrapper>
    </AuthWrapper>
    </body>
    </html>
    );
    }

    Returns the current JWT access token (or null if not loaded / logged out). Use it for conditional rendering or passing to custom fetch calls.

    import { useToken } from "naystack/auth/client";

    export default function Home() {
    const token = useToken();

    return (
    <Link href={token ? "/dashboard" : "/signup"}>
    <button>{token ? "Dashboard" : "Get Started"}</button>
    </Link>
    );
    }

    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.

    import { useSignUp } from "naystack/auth/client";

    function SignUpForm() {
    const signUp = useSignUp();

    const handleSubmit = async (data: { name: string; email: string; password: string }) => {
    const error = await signUp(data);
    if (error) {
    setMessage(error);
    } else {
    router.replace("/dashboard");
    }
    };
    }

    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.

    import { useLogin } from "naystack/auth/client";

    function LoginForm() {
    const login = useLogin();

    const handleSubmit = async (data: { email: string; password: string }) => {
    const error = await login(data);
    if (error) {
    form.setError("password", { message: error });
    } else {
    router.replace("/dashboard");
    }
    };
    }

    Returns a function that logs the user out. Clears the token immediately and sends DELETE to the auth endpoint.

    import { useLogout } from "naystack/auth/client";

    function LogoutButton() {
    const logout = useLogout();

    return (
    <button onClick={() => { logout(); router.push("/login"); }}>
    Log out
    </button>
    );
    }

    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.

    import { getContext } from "naystack/auth";

    export const POST = async (req: NextRequest) => {
    const ctx = getContext(req);
    if (!ctx?.userId) return new NextResponse("Unauthorized", { status: 401 });

    // ctx.userId is available for authenticated operations
    const chats = await db.select().from(ChatTable).where(eq(ChatTable.userId, ctx.userId));
    return NextResponse.json(chats);
    };

    Server-side function to read the refresh token from cookies. Useful in Server Components and layouts to check if the user is logged in.

    import { getRefreshToken } from "naystack/auth";
    import { redirect } from "next/navigation";

    export default async function ProtectedLayout({ children }: { children: React.ReactNode }) {
    const token = await getRefreshToken();
    if (!token) return redirect("/login");
    return <div>{children}</div>;
    }

    Checks if the current request has a valid refresh cookie. Optionally redirects to the given URL if not authorized.

    import { checkAuthStatus } from "naystack/auth";

    // In a Server Component:
    await checkAuthStatus("/login"); // Redirects to /login if not authorized
    import { initGoogleAuth } from "naystack/auth";

    export const { GET } = initGoogleAuth({
    getUserIdFromEmail: async (googleUser) => {
    // Find or create user by Google email
    return findOrCreateUserByEmail(googleUser.email!);
    },
    redirectURL: "/dashboard",
    errorRedirectURL: "/login",
    });
    import { initInstagramAuth } from "naystack/auth";

    export const { GET, getRefreshedAccessToken } = initInstagramAuth({
    onUser: async (igUser, appUserId, accessToken) => {
    await saveInstagramUser(appUserId, igUser, accessToken);
    },
    successRedirectURL: "/dashboard",
    errorRedirectURL: "/login",
    refreshKey: process.env.REFRESH_KEY!,
    });

    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.

    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).

    // app/api/(graphql)/User/resolvers/get-current-user.ts
    import { query } from "naystack/graphql";

    export default query(
    async (ctx) => {
    if (!ctx.userId) return null;
    const [user] = await db
    .select()
    .from(UserTable)
    .where(eq(UserTable.id, ctx.userId));
    return user || null;
    },
    {
    output: User, // GraphQL return type (type-graphql class)
    outputOptions: { nullable: true }, // Return type is nullable
    },
    );

    With input and authorization:

    // app/api/(graphql)/Feedback/resolvers/submit-feedback.ts
    import { query } from "naystack/graphql";

    export default query(
    async (ctx, input: SubmitFeedbackInput) => {
    await db.insert(FeedbackTable).values({
    userId: ctx.userId, // guaranteed non-null when authorized: true
    score: input.score,
    text: input.text,
    });
    return true;
    },
    {
    output: Boolean,
    input: SubmitFeedbackInput, // GraphQL input type (type-graphql @InputType class)
    authorized: true, // Requires authenticated user (ctx.userId non-null)
    mutation: true, // Registers as a Mutation (default is Query)
    },
    );

    Use field() to define resolvers for computed fields on a parent type. The first argument is the parent object.

    // app/api/(graphql)/Property/resolvers/seller-field.ts
    import { field } from "naystack/graphql";

    export default field(
    async (property: PropertyDB) => {
    if (!property.sellerId) return null;
    const [seller] = await db
    .select()
    .from(ContactTable)
    .where(eq(ContactTable.id, property.sellerId));
    return seller || null;
    },
    {
    output: ContactGQL,
    outputOptions: { nullable: true },
    },
    );

    Use QueryLibrary() for queries/mutations and FieldLibrary() for field resolvers. Pass the result to initGraphQLServer.

    // app/api/(graphql)/User/graphql.ts
    import { QueryLibrary, FieldLibrary } from "naystack/graphql";
    import getCurrentUser from "./resolvers/get-current-user";
    import onboardUser from "./resolvers/onboard-user";
    import updateUser from "./resolvers/update-user";
    import organizations from "./resolvers/organizations-field";
    import { User } from "./types";

    // Each key becomes a Query or Mutation field name in the schema
    export const UserResolvers = QueryLibrary({
    getCurrentUser,
    onboardUser,
    updateUser,
    });

    // Each key becomes a field resolver on the User type
    export const UserFieldResolvers = FieldLibrary<UserDB>(User, {
    organizations,
    });
    // app/api/(graphql)/route.ts
    import { initGraphQLServer } from "naystack/graphql";
    import { UserResolvers, UserFieldResolvers } from "./User/graphql";
    import { ChatResolvers } from "./Chat/graphql";
    import { FeedbackResolvers } from "./Feedback/graphql";

    export const { GET, POST } = await initGraphQLServer({
    resolvers: [UserResolvers, UserFieldResolvers, ChatResolvers, FeedbackResolvers],
    });

    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.

    Use GQLError() to throw structured GraphQL errors from resolvers:

    import { GQLError } from "naystack/graphql";

    // In a resolver:
    if (!input.email) throw GQLError(400); // "Please provide all required inputs"
    if (!ctx.userId) throw GQLError(403); // "You are not allowed to perform this action"
    if (!deal) throw GQLError(404, "Deal not found"); // Custom message

    Infer the return type of a query definition. Use it to type component props that receive query results.

    import type { QueryResponseType } from "naystack/graphql";
    import type getCurrentUser from "@/app/api/(graphql)/User/resolvers/get-current-user";
    import type getDeal from "@/app/api/(graphql)/Deal/queries/get-deal";

    interface DealDetailsProps {
    user: QueryResponseType<typeof getCurrentUser>;
    deal: QueryResponseType<typeof getDeal>;
    }

    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.

    // In a Server Component:
    const user = await getCurrentUser.authCall();
    const planets = await getPlanets.authCall();

    Wraps a client component and injects server-fetched data via Suspense. The component receives { data, loading } as props.

    // app/(dashboard)/chat/page.tsx
    import { Injector } from "naystack/graphql/server";
    import getCurrentUser from "@/app/api/(graphql)/User/resolvers/get-current-user";
    import getChats from "@/app/api/(graphql)/Chat/resolvers/get-chats";
    import { ChatWindow } from "./components/chat-window";

    export default async function ChatPage() {
    return (
    <Injector
    fetch={async () => {
    const user = await getCurrentUser.authCall();
    const chats = await getChats.authCall();
    return { user, chats };
    }}
    Component={ChatWindow}
    />
    );
    }

    The ChatWindow component receives { data, loading }:

    // components/chat-window.tsx
    export function ChatWindow({ data, loading }: { data?: { user: ...; chats: ... }; loading: boolean }) {
    if (loading) return <Spinner />;
    return <div>{data?.user.name}'s chats: {data?.chats.length}</div>;
    }

    Run a raw GraphQL query on the server using the registered Apollo client. Cookies are sent automatically.

    import { query } from "naystack/graphql/server";

    const data = await query(GetUserDocument, {
    variables: { id: userId },
    revalidate: 60, // Cache for 60s (Next.js ISR)
    tags: ["user"], // For on-demand revalidation
    });

    Wrap your app with ApolloWrapper (inside AuthWrapper) so client components can use GraphQL hooks:

    // app/layout.tsx
    import { AuthWrapper } from "naystack/auth/client";
    import { ApolloWrapper } from "naystack/graphql/client";

    export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
    <html lang="en">
    <body>
    <AuthWrapper>
    <ApolloWrapper>{children}</ApolloWrapper>
    </AuthWrapper>
    </body>
    </html>
    );
    }

    Hook to run a GraphQL query with the current user's token. Returns [refetch, { data, loading, error }].

    import { useAuthQuery } from "naystack/graphql/client";
    import { GET_SUMMARY } from "@/constants/graphql/queries";

    function SummaryCard({ type }: { type: string }) {
    const [getSummary, { loading, data }] = useAuthQuery(GET_SUMMARY);

    const handleFetch = async () => {
    const result = await getSummary({ type });
    if (result.data?.getSummary) {
    setSummary(result.data.getSummary);
    }
    };

    return <button onClick={handleFetch} disabled={loading}>Get Summary</button>;
    }

    Hook to run a GraphQL mutation with the current user's token. Returns [mutate, { data, loading, error }].

    import { useAuthMutation } from "naystack/graphql/client";
    import { CREATE_DEAL } from "@/lib/gql/mutations";

    function CreateDealModal({ propertyId }: { propertyId: number }) {
    const [createDeal, { loading }] = useAuthMutation(CREATE_DEAL);

    const onSubmit = async (values: FormFields) => {
    const response = await createDeal({
    propertyId,
    share: Number(values.share),
    targetProfit: Number(values.targetProfit),
    });
    const dealId = response.data?.createDeal;
    if (dealId) router.push(`/deals/${dealId}`);
    };

    return <form onSubmit={handleSubmit(onSubmit)}>...</form>;
    }

    Naystack simplifies AWS S3 file uploads with presigned URLs and client-side helpers. AWS credentials are read from environment variables automatically.

    // app/api/(rest)/file/route.ts
    import { setupFileUpload } from "naystack/file";

    export const { PUT } = setupFileUpload({
    // Called after each successful upload. Return value is sent in the response as `onUploadResponse`.
    onUpload: async ({ url, type, userId, data }) => {
    if (type === "DealDocument" && url) {
    const payload = data as { dealId: number; fileName: string; category: string };
    const [row] = await db
    .insert(DealDocumentsTable)
    .values({ dealId: payload.dealId, fileURL: url, fileName: payload.fileName, category: payload.category })
    .returning();
    return row ?? {};
    }
    return {};
    },
    // Optional: customize the S3 key (defaults to UUID)
    getKey: async ({ type, userId }) => `${type}/${userId}/${crypto.randomUUID()}`,
    });

    The setupFileUpload also returns server-side helpers:

    • uploadFile(keys, { url?, blob? }) — Upload a file from a URL or Blob to S3.
    • deleteFile(url) — Delete a file by its full S3 URL.
    • getUploadURL(keys) — Get a presigned PUT URL.
    • getDownloadURL(keys) — Get the public download URL.
    import { useFileUpload } from "naystack/file/client";

    function FileUploader({ dealId }: { dealId: number }) {
    const uploadFile = useFileUpload();
    const [uploading, setUploading] = useState(false);

    const handleUpload = async (file: File) => {
    setUploading(true);
    try {
    const result = await uploadFile(file, "DealDocument", {
    dealId,
    fileName: file.name,
    category: "Contract",
    });
    if (result?.url) {
    console.log("Uploaded:", result.url);
    router.refresh();
    }
    } finally {
    setUploading(false);
    }
    };

    return <input type="file" onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])} />;
    }

    The setupSEO utility creates a metadata factory for Next.js. Call it once with your site defaults, then use the returned function per-page.

    // lib/utils/seo.ts
    import { setupSEO } from "naystack/client";

    export const getSEO = setupSEO({
    title: "My App - Tagline",
    description: "Description of my application.",
    siteName: "My App",
    themeColor: "#5b9364",
    });

    // In a page:
    export const metadata = getSEO("Dashboard", "Your personalized dashboard");
    // Produces: title = "Dashboard • My App", description = "Your personalized dashboard"

    Triggers a callback when a DOM element enters the viewport. Returns a ref to attach to the observed element.

    import { useVisibility } from "naystack/client";

    function LazySection() {
    const ref = useVisibility(() => loadMoreData());
    return <section ref={ref}>...</section>;
    }

    Responsive media query hook. Returns true/false or null during SSR.

    import { useBreakpoint } from "naystack/client";

    function ResponsiveNav() {
    const isMobile = useBreakpoint("(max-width: 639px)");
    if (isMobile === null) return <Skeleton />;
    return isMobile ? <MobileNav /> : <DesktopNav />;
    }

    Simplified access to Instagram Graph API and Threads API.

    import {
    getInstagramUser,
    getInstagramMedia,
    getInstagramConversations,
    getInstagramConversation,
    getInstagramMessage,
    sendInstagramMessage,
    setupInstagramWebhook,
    } from "naystack/socials";

    // Fetch the authenticated user's profile
    const user = await getInstagramUser(accessToken);
    // => { username: "johndoe", followers_count: 1234, media_count: 56 }

    // Fetch recent media
    const media = await getInstagramMedia(accessToken, undefined, 10);
    // => { data: [{ like_count: 5, comments_count: 2, permalink: "..." }, ...] }

    // Fetch conversations with pagination
    const convos = await getInstagramConversations(accessToken, 25);
    for (const convo of convos.data ?? []) {
    console.log(convo.participants, convo.messages);
    }
    if (convos.fetchMore) {
    const nextPage = await convos.fetchMore();
    }

    // Send a message
    await sendInstagramMessage(accessToken, recipientId, "Hello!");

    // Webhook setup (app/api/webhooks/instagram/route.ts)
    export const { GET, POST } = setupInstagramWebhook({
    secret: process.env.WEBHOOK_SECRET!,
    callback: async (type, value, id) => {
    console.log("Webhook event:", type, value, id);
    },
    });
    import {
    getThread,
    getThreads,
    getThreadsReplies,
    createThreadsPost,
    createThread,
    setupThreadsWebhook,
    } from "naystack/socials";

    // Fetch user's threads
    const threads = await getThreads(accessToken);
    // => [{ text: "Hello world", permalink: "...", username: "johndoe" }]

    // Create and publish a single post
    const postId = await createThreadsPost(accessToken, "Hello from Naystack!");

    // Create a thread (sequence of posts)
    const firstPostId = await createThread(accessToken, [
    "First post in thread",
    "Second post (reply to first)",
    "Third post (reply to second)",
    ]);

    // Webhook setup (app/api/webhooks/threads/route.ts)
    export const { GET, POST } = setupThreadsWebhook({
    secret: process.env.WEBHOOK_SECRET!,
    callback: async (field, value) => {
    console.log("Threads event:", field, value);
    return true; // Return false to respond with 500
    },
    });

    Naystack reads configuration from environment variables. Set the ones you need based on which modules you use.

    SIGNING_KEY=your-jwt-signing-key
    REFRESH_KEY=your-jwt-refresh-key
    NEXT_PUBLIC_EMAIL_AUTH_ENDPOINT=/api/email
    NEXT_PUBLIC_GRAPHQL_ENDPOINT=/api/graphql
    NEXT_PUBLIC_FILE_ENDPOINT=/api/file
    NEXT_PUBLIC_BASE_URL=https://yourapp.com
    GOOGLE_CLIENT_ID=your-google-client-id
    GOOGLE_CLIENT_SECRET=your-google-client-secret
    NEXT_PUBLIC_GOOGLE_AUTH_ENDPOINT=/api/google
    INSTAGRAM_CLIENT_ID=your-instagram-client-id
    INSTAGRAM_CLIENT_SECRET=your-instagram-client-secret
    NEXT_PUBLIC_INSTAGRAM_AUTH_ENDPOINT=/api/instagram
    AWS_REGION=us-east-1
    AWS_BUCKET=your-bucket-name
    AWS_ACCESS_KEY_ID=your-access-key-id
    AWS_ACCESS_KEY_SECRET=your-secret-access-key
    TURNSTILE_KEY=cloudflare-turnstile-secret-key
    NODE_ENV=production