alinaqi/maggy/supabase-node

Trust tier 2
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Express/Hono with Supabase and Drizzle ORM

Install

npx vskill@latest install alinaqi/maggy/supabase-node

SKILL.md

Supabase + Node.js Skill

Express/Hono patterns with Supabase Auth and Drizzle ORM.

Sources: Supabase JS Client | Drizzle ORM


Core Principle

Drizzle for queries, Supabase for auth/storage, middleware for validation.

Use Drizzle ORM for type-safe database access. Use Supabase client for auth verification, storage, and realtime. Express or Hono for the API layer.


Project Structure

project/
├── src/
│ ├── routes/
│ │ ├── index.ts # Route aggregator
│ │ ├── auth.ts
│ │ ├── posts.ts
│ │ └── users.ts
│ ├── middleware/
│ │ ├── auth.ts # JWT validation
│ │ ├── error.ts # Error handler
│ │ └── validate.ts # Request validation
│ ├── db/
│ │ ├── index.ts # Drizzle client
│ │ ├── schema.ts # Schema definitions
│ │ └── queries/ # Query functions
│ ├── lib/
│ │ ├── supabase.ts # Supabase client
│ │ └── config.ts # Environment config
│ ├── types/
│ │ └── express.d.ts # Express type extensions
│ └── index.ts # App entry point
├── supabase/
│ ├── migrations/
│ └── config.toml
├── drizzle.config.ts
├── package.json
├── tsconfig.json
└── .env

Setup

Install Dependencies
bash
npm install express cors helmet dotenv @supabase/supabase-js drizzle-orm postgres zod
npm install -D typescript @types/express @types/cors @types/node tsx drizzle-kit
package.json Scripts
json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
}
Environment Variables
bash
# .env
PORT=3000
NODE_ENV=development
# Supabase
SUPABASE_URL=http://localhost:54321
SUPABASE_ANON_KEY=<from supabase start>
SUPABASE_SERVICE_ROLE_KEY=<from supabase start>
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:54322/postgres

Configuration

src/lib/config.ts
typescript
import { z } from 'zod';
import dotenv from 'dotenv';
dotenv.config();
const envSchema = z.object({
PORT: z.string().default('3000'),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
SUPABASE_URL: z.string().url(),
SUPABASE_ANON_KEY: z.string(),
SUPABASE_SERVICE_ROLE_KEY: z.string(),
DATABASE_URL: z.string(),
});
export const config = envSchema.parse(process.env);

Database Setup

drizzle.config.ts
typescript
import { defineConfig } from 'drizzle-kit';
import { config } from './src/lib/config';
export default defineConfig({
schema: './src/db/schema.ts',
out: './supabase/migrations',
dialect: 'postgresql',
dbCredentials: {
url: config.DATABASE_URL,
},
schemaFilter: ['public'],
});
src/db/index.ts
typescript
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
import { config } from '../lib/config';
const client = postgres(config.DATABASE_URL, {
prepare: false, // Required for Supabase pooling
});
export const db = drizzle(client, { schema });
src/db/schema.ts
typescript
import {
pgTable,
uuid,
text,
timestamp,
boolean,
} from 'drizzle-orm/pg-core';
export const profiles = pgTable('profiles', {
id: uuid('id').primaryKey(),
email: text('email').notNull(),
name: text('name'),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
authorId: uuid('author_id').references(() => profiles.id).notNull(),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Type exports
export type Profile = typeof profiles.$inferSelect;
export type NewProfile = typeof profiles.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;

Supabase Client

src/lib/supabase.ts
typescript
import { createClient, SupabaseClient, User } from '@supabase/supabase-js';
import { config } from './config';
// Client with anon key (respects RLS)
export const supabase = createClient(
config.SUPABASE_URL,
config.SUPABASE_ANON_KEY
);
// Admin client (bypasses RLS)
export const supabaseAdmin = createClient(
config.SUPABASE_URL,
config.SUPABASE_SERVICE_ROLE_KEY,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
}
);
// Verify JWT and get user
export async function verifyToken(token: string): Promise<User | null> {
const { data: { user }, error } = await supabase.auth.getUser(token);
if (error || !user) {
return null;
}
return user;
}

Type Extensions

src/types/express.d.ts
typescript
import { User } from '@supabase/supabase-js';
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
export {};

Middleware

src/middleware/auth.ts
typescript
import { Request, Response, NextFunction } from 'express';
import { verifyToken } from '../lib/supabase';
export async function requireAuth(
req: Request,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing authorization header' });
}
const token = authHeader.split(' ')[1];
const user = await verifyToken(token);
if (!user) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = user;
next();
}
// Optional auth - continues even without token
export async function optionalAuth(
req: Request,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
req.user = await verifyToken(token) ?? undefined;
}
next();
}
src/middleware/error.ts
typescript
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
this.name = 'AppError';
}
}
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
console.error(err);
if (err instanceof AppError) {
return res.status(err.statusCode).json({ error: err.message });
}
return res.status(500).json({ error: 'Internal server error' });
}
src/middleware/validate.ts
typescript
import { Request, Response, NextFunction } from 'express';
import { z, ZodSchema } from 'zod';
export function validate<T extends ZodSchema>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors,
});
}
next(error);
}
};
}

Routes

src/routes/auth.ts
typescript
import { Router } from 'express';
import { z } from 'zod';
import { supabase } from '../lib/supabase';
import { validate } from '../middleware/validate';
const router = Router();
const signUpSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const signInSchema = z.object({
email: z.string().email(),
password: z.string(),
});
router.post('/signup', validate(signUpSchema), async (req, res, next) => {
try {
const { email, password } = req.body;
const { data, error } = await supabase.auth.signUp({
email,
password,
});
if (error) {
return res.status(400).json({ error: error.message });
}
return res.status(201).json({
user: data.user,
session: data.session,
});
} catch (error) {
next(error);
}
});
router.post('/signin', validate(signInSchema), async (req, res, next) => {
try {
const { email, password } = req.body;
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return res.status(401).json({ error: 'Invalid credentials' });
}
return res.json({
user: data.user,
session: data.session,
});
} catch (error) {
next(error);
}
});
router.post('/signout', async (req, res) => {
await supabase.auth.signOut();
return res.json({ message: 'Signed out' });
});
router.post('/refresh', async (req, res, next) => {
try {
const { refresh_token } = req.body;
const { data, error } = await supabase.auth.refreshSession({
refresh_token,
});
if (error) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
return res.json({
session: data.session,
});
} catch (error) {
next(error);
}
});
export default router;
src/routes/posts.ts
typescript
import { Router } from 'express';
import { z } from 'zod';
import { eq, desc } from 'drizzle-orm';
import { db } from '../db';
import { posts, Post } from '../db/schema';
import { requireAuth, optionalAuth } from '../middleware/auth';
import { validate } from '../middleware/validate';
import { AppError } from '../middleware/error';
const router = Router();
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().optional(),
published: z.boolean().default(false),
});
const updatePostSchema = createPostSchema.partial();
// List all published posts
router.get('/', optionalAuth, async (req, res, next) => {
try {
const result = await db
.select()
.from(posts)
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt));
return res.json(result);
} catch (error) {
next(error);
}
});
// List user's posts
router.get('/me', requireAuth, async (req, res, next) => {
try {
const result = await db
.select()
.from(posts)
.where(eq(posts.authorId, req.user!.id))
.orderBy(desc(posts.createdAt));
return res.json(result);
} catch (error) {
next(error);
}
});
// Get single post
router.get('/:id', async (req, res, next) => {
try {
const [post] = await db
.select()
.from(posts)
.where(eq(posts.id, req.params.id))
.limit(1);
if (!post) {
throw new AppError(404, 'Post not found');
}
return res.json(post);
} catch (error) {
next(error);
}
});
// Create post
router.post('/', requireAuth, validate(createPostSchema), async (req, res, next) => {
try {
const [post] = await db
.insert(posts)
.values({
...req.body,
authorId: req.user!.id,
})
.returning();
return res.status(201).json(post);
} catch (error) {
next(error);
}
});
// Update post
router.patch('/:id', requireAuth, validate(updatePostSchema), async (req, res, next) => {
try {
const [post] = await db
.update(posts)
.set(req.body)
.where(eq(posts.id, req.params.id))
.returning();
if (!post) {
throw new AppError(404, 'Post not found');
}
return res.json(post);
} catch (error) {
next(error);
}
});
// Delete post
router.delete('/:id', requireAuth, async (req, res, next) => {
try {
const [post] = await db
.delete(posts)
.where(eq(posts.id, req.params.id))
.returning();
if (!post) {
throw new AppError(404, 'Post not found');
}
return res.status(204).send();
} catch (error) {
next(error);
}
});
export default router;

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
707
Repository forks
57
7-day trend score
33

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/alinaqi/maggy/supabase-node/badge)](https://verified-skill.com/skills/alinaqi/maggy/supabase-node)

Something wrong with this skill or its page? Tell us.