Skip to main content

Overview

Polaris IDE uses Stack Auth for authentication, providing a complete auth solution with pre-built UI components, OAuth integrations, and machine-to-machine (M2M) authentication for the desktop app.
Polaris IDE migrated from Clerk to Stack Auth for better Convex integration and first-class Electron support.

Features

Pre-built UI

Beautiful, customizable sign-in and sign-up pages with no code required

OAuth Providers

GitHub, Google, and other OAuth providers supported out of the box

Convex Integration

Seamless JWT authentication with Convex real-time database

M2M Authentication

Machine-to-machine auth for Electron desktop app

Session Management

Secure cookie-based sessions with automatic refresh

User Management

Built-in user profiles and account settings

Setup

Create Stack Auth Project

1

Sign up for Stack Auth

Go to https://app.stack-auth.com and create an account.
2

Create a New Project

Click “New Project” and enter your project details:
  • Project Name: Polaris IDE
  • Environment: Development (create production later)
3

Copy Credentials

From the project dashboard, copy:
  • Project ID
  • Publishable Client Key
  • Secret Server Key
4

Configure OAuth Providers

Enable OAuth providers (optional):
  1. Go to “Authentication” > “OAuth Providers”
  2. Enable GitHub, Google, or others
  3. Follow provider-specific setup instructions

Environment Variables

Add Stack Auth credentials to .env.local:
# Stack Auth
NEXT_PUBLIC_STACK_PROJECT_ID=your_project_id
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=your_publishable_key
STACK_SECRET_SERVER_KEY=your_secret_key
Never commit STACK_SECRET_SERVER_KEY to version control. Keep it in .env.local (gitignored).

Client-Side Setup

Stack Auth client configuration (stack/client.ts:8):
import { StackClientApp } from "@stackframe/stack";

export const stackClientApp = new StackClientApp({
  projectId: process.env.NEXT_PUBLIC_STACK_PROJECT_ID!,
  publishableClientKey: process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY!,
  tokenStore: "nextjs-cookie",
  urls: {
    signIn: "/handler/sign-in",
    signUp: "/handler/sign-up",
    afterSignIn: "/",
    afterSignUp: "/",
    accountSettings: "/handler/account-settings",
  },
});

Provider Setup

Wrap your app with the Stack provider (src/components/providers.tsx):
import { StackProvider } from "@stackframe/stack";
import { stackClientApp } from "@/stack/client";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <StackProvider app={stackClientApp}>
      {children}
    </StackProvider>
  );
}

Get Current User

Access the authenticated user in client components:
import { useUser } from "@stackframe/stack";

function MyComponent() {
  const user = useUser();
  
  if (!user) {
    return <div>Not authenticated</div>;
  }
  
  return (
    <div>
      <p>Welcome, {user.displayName}!</p>
      <p>Email: {user.primaryEmail}</p>
    </div>
  );
}

Server-Side Setup

Stack Auth server configuration (stack/server.ts:4):
import "server-only";
import { StackServerApp } from "@stackframe/stack";

export const stackServerApp = new StackServerApp({
  tokenStore: "nextjs-cookie",
  urls: {
    signIn: "/handler/sign-in",
    signUp: "/handler/sign-up",
    afterSignIn: "/",
    afterSignUp: "/",
    accountSettings: "/handler/account-settings",
  },
});

API Route Authentication

Protect API routes using the requireAuth helper (src/lib/stack-auth-api.ts):
import { requireAuth } from "@/lib/stack-auth-api";

export async function POST(request: Request) {
  const { user, userId, getToken, response } = await requireAuth();
  
  if (!user) {
    return response; // Returns 401 Unauthorized
  }
  
  // User is authenticated, proceed with logic
  const convexToken = await getToken();
  
  // Use convexToken to authenticate with Convex
}

Get User in Server Components

Access the current user in server components:
import { stackServerApp } from "@/stack/server";

export default async function ServerComponent() {
  const user = await stackServerApp.getUser();
  
  if (!user) {
    redirect("/handler/sign-in");
  }
  
  return <div>Hello, {user.displayName}!</div>;
}

Authentication Flow

Sign In

Stack Auth provides a pre-built sign-in page at /handler/sign-in:
// src/app/handler/[...stack]/page.tsx
import { StackHandler } from "@stackframe/stack";
import { stackServerApp } from "@/stack/server";

export default function Handler(props: any) {
  return <StackHandler fullPage app={stackServerApp} {...props} />;
}
This renders:
  • Email/password sign-in form
  • OAuth provider buttons (if configured)
  • “Forgot password” link
  • “Sign up” link

Sign Up

Similarly, /handler/sign-up provides a registration form:
  • Email/password registration
  • OAuth provider buttons
  • Email verification (if enabled)
  • Automatic redirect after signup

Account Settings

Users can manage their account at /handler/account-settings:
  • Update profile information
  • Change password
  • Manage connected OAuth accounts
  • Delete account

Convex Integration

Stack Auth integrates seamlessly with Convex for real-time database access.

Convex Auth Configuration

Configure Convex to accept Stack Auth JWTs (convex/auth.config.ts:3):
import { getConvexProvidersConfig } from "@stackframe/stack";

export default {
  providers: getConvexProvidersConfig({
    projectId: process.env.NEXT_PUBLIC_STACK_PROJECT_ID || "",
  }),
};

Client-Side Convex Setup

Provide Convex authentication using Stack tokens:
import { ConvexProvider } from "convex/react";
import { stackClientApp } from "@/stack/client";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
convex.setAuth(stackClientApp.getConvexClientAuth());

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <StackProvider app={stackClientApp}>
      <ConvexProvider client={convex}>
        {children}
      </ConvexProvider>
    </StackProvider>
  );
}

Verify Authentication in Convex

Validate users in Convex queries and mutations (convex/auth.ts):
import { QueryCtx, MutationCtx } from "./_generated/server";

export async function verifyAuth(ctx: QueryCtx | MutationCtx) {
  const identity = await ctx.auth.getUserIdentity();
  
  if (!identity) {
    throw new Error("Unauthenticated");
  }
  
  return identity;
}
Use in queries/mutations:
export const getProjects = query({
  handler: async (ctx) => {
    const identity = await verifyAuth(ctx);
    const userId = identity.subject; // Stack Auth user ID
    
    return await ctx.db
      .query("projects")
      .withIndex("by_owner", (q) => q.eq("ownerId", userId))
      .collect();
  },
});

User Management

User Schema

Convex user records (convex/schema.ts:8):
users: defineTable({
  stackUserId: v.string(),  // Stack Auth user ID
  email: v.string(),
  autumnCustomerId: v.optional(v.string()),
  subscriptionStatus: v.optional(
    v.union(
      v.literal("free"),
      v.literal("trialing"),
      v.literal("active"),
      v.literal("paused"),
      v.literal("canceled"),
      v.literal("past_due")
    )
  ),
  subscriptionTier: v.optional(
    v.union(
      v.literal("free"),
      v.literal("pro_monthly"),
      v.literal("pro_yearly")
    )
  ),
  projectLimit: v.number(),
  createdAt: v.number(),
  updatedAt: v.number(),
})
  .index("by_stack_user", ["stackUserId"])

Create or Get User

Automatically create user records on first sign-in (convex/users.ts:19):
export const getOrCreateUser = mutation({
  args: {},
  handler: async (ctx) => {
    const identity = await verifyAuth(ctx);
    const stackUserId = identity.subject;
    const email = identity.email || '';
    
    const existingUser = await ctx.db
      .query('users')
      .withIndex('by_stack_user', (q) => q.eq('stackUserId', stackUserId))
      .first();
    
    if (existingUser) {
      return existingUser;
    }
    
    // Create new user with free tier
    const userId = await ctx.db.insert('users', {
      stackUserId,
      email,
      subscriptionStatus: 'free',
      subscriptionTier: 'free',
      projectLimit: 10,
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
    
    return await ctx.db.get(userId);
  },
});

Desktop App Authentication (M2M)

The Electron desktop app uses machine-to-machine (M2M) authentication for server-side operations.

M2M Token Generation

Generate M2M tokens using the Stack Auth API (src/lib/electron/stack-auth.ts):
import { stackServerApp } from "@/stack/server";

export async function getStackM2MToken(): Promise<string> {
  const token = await stackServerApp.getServerToken();
  return token;
}

Token Storage

Tokens are stored securely using electron-store:
import Store from 'electron-store';

const store = new Store();

export function storeAuthToken(token: string, expiresAt: number) {
  store.set('auth', {
    token,
    expiresAt,
  });
}

export function getStoredAuthToken(): { token: string; expiresAt: number } | null {
  return store.get('auth') as any;
}

export function clearAuthToken() {
  store.delete('auth');
}

Token Refresh

Automatically refresh expired tokens:
export async function getConvexAuthForElectron(): Promise<string> {
  const stored = getStoredAuthToken();
  
  if (stored && stored.expiresAt > Date.now()) {
    return stored.token;
  }
  
  // Token expired or missing, get new one
  const newToken = await getStackM2MToken();
  const expiresAt = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
  
  storeAuthToken(newToken, expiresAt);
  return newToken;
}

Migration from Clerk

Polaris IDE previously used Clerk for authentication. The migration to Stack Auth is documented in STACK_AUTH_MIGRATION_SUMMARY.md.

Key Changes

Before (Clerk):
import { ClerkProvider } from "@clerk/nextjs";

<ClerkProvider>
  {children}
</ClerkProvider>
After (Stack Auth):
import { StackProvider } from "@stackframe/stack";

<StackProvider app={stackClientApp}>
  {children}
</StackProvider>

Backward Compatibility

The schema includes both stackUserId and clerkId fields during the migration period:
users: defineTable({
  stackUserId: v.string(),           // NEW - Stack Auth
  clerkId: v.optional(v.string()),   // LEGACY - Clerk
  // ...
})
  .index("by_stack_user", ["stackUserId"])
  .index("by_clerk", ["clerkId"])  // Kept for migration

Security Features

All JWTs are verified server-side before granting access:
  • Signature validation
  • Expiration checking
  • Issuer verification
Stack Auth implements rate limiting on:
  • Sign-in attempts
  • Password reset requests
  • Email verification sends
Cross-site request forgery protection is built-in:
  • CSRF tokens for state-changing operations
  • Origin validation

Troubleshooting

Issue: Console error about missing Stack Auth environment variablesSolution:
  1. Ensure .env.local exists with all required variables
  2. Restart the development server after adding variables
  3. Check variable names match exactly (including NEXT_PUBLIC_ prefix)
Issue: Convex queries/mutations fail with “Unauthenticated”Solution:
  1. Verify convex/auth.config.ts is correctly configured
  2. Check Convex client has auth set: convex.setAuth(...)
  3. Ensure user is signed in (check with useUser() hook)
  4. Verify Stack Auth project ID matches in all configs
Issue: Infinite redirect between sign-in page and protected routesSolution:
  1. Check that authentication URLs are correct in Stack config
  2. Verify middleware is not blocking auth handler routes
  3. Clear browser cookies and try again
Issue: Electron app cannot authenticate with ConvexSolution:
  1. Verify STACK_SECRET_SERVER_KEY is set
  2. Check M2M token generation is working
  3. Ensure electron-store can write to disk
  4. Check logs in ~/Library/Logs/Polaris IDE/ (macOS)

Next Steps

Convex Integration

Learn how Convex stores and syncs data

GitHub Integration

Import and export projects to GitHub

Build docs developers (and LLMs) love