Skip to main content
Glama

Karakeep MCP server

by karakeep-app
subscriptions.ts12.3 kB
// Thanks to @t3dotgg for the recommendations (https://github.com/t3dotgg/stripe-recommendations)! import { TRPCError } from "@trpc/server"; import { count, eq, sum } from "drizzle-orm"; import Stripe from "stripe"; import { z } from "zod"; import { assets, bookmarks, subscriptions, users } from "@karakeep/db/schema"; import serverConfig from "@karakeep/shared/config"; import { authedProcedure, Context, publicProcedure, router } from "../index"; const stripe = serverConfig.stripe.secretKey ? new Stripe(serverConfig.stripe.secretKey, { apiVersion: "2025-06-30.basil", }) : null; function requireStripeConfig() { if (!stripe || !serverConfig.stripe.priceId) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Stripe is not configured. Please contact your administrator.", }); } return { stripe, priceId: serverConfig.stripe.priceId }; } // Taken from https://github.com/t3dotgg/stripe-recommendations const allowedEvents: Stripe.Event.Type[] = [ "checkout.session.completed", "customer.subscription.created", "customer.subscription.updated", "customer.subscription.deleted", "customer.subscription.paused", "customer.subscription.resumed", "customer.subscription.pending_update_applied", "customer.subscription.pending_update_expired", "customer.subscription.trial_will_end", "invoice.paid", "invoice.payment_failed", "invoice.payment_action_required", "invoice.upcoming", "invoice.marked_uncollectible", "invoice.payment_succeeded", "payment_intent.succeeded", "payment_intent.payment_failed", "payment_intent.canceled", ]; async function syncStripeDataToDatabase(customerId: string, db: Context["db"]) { if (!stripe) { throw new Error("Stripe is not configured"); } const existingSubscription = await db.query.subscriptions.findFirst({ where: eq(subscriptions.stripeCustomerId, customerId), }); if (!existingSubscription) { console.error( `ERROR: No subscription found for customer with this ID ${customerId}`, ); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "No subscription found for customer with this ID", }); } try { const subscriptionsList = await stripe.subscriptions.list({ customer: customerId, limit: 1, status: "all", }); if (subscriptionsList.data.length === 0) { await db.transaction(async (trx) => { await trx .update(subscriptions) .set({ status: "canceled", tier: "free", stripeSubscriptionId: null, priceId: null, cancelAtPeriodEnd: false, startDate: null, endDate: null, }) .where(eq(subscriptions.stripeCustomerId, customerId)); // Update user quotas to free tier limits and disable browser crawling await trx .update(users) .set({ bookmarkQuota: serverConfig.quotas.free.bookmarkLimit, storageQuota: serverConfig.quotas.free.assetSizeBytes, browserCrawlingEnabled: serverConfig.quotas.free.browserCrawlingEnabled, }) .where(eq(users.id, existingSubscription.userId)); }); return; } const subscription = subscriptionsList.data[0]; const subscriptionItem = subscription.items.data[0]; const subData = { stripeSubscriptionId: subscription.id, status: subscription.status, tier: subscription.status === "active" || subscription.status === "trialing" ? ("paid" as const) : ("free" as const), priceId: subscription.items.data[0]?.price.id || null, cancelAtPeriodEnd: subscription.cancel_at_period_end, startDate: subscriptionItem.current_period_start ? new Date(subscriptionItem.current_period_start * 1000) : null, endDate: subscriptionItem.current_period_end ? new Date(subscriptionItem.current_period_end * 1000) : null, }; await db.transaction(async (trx) => { await trx .update(subscriptions) .set(subData) .where(eq(subscriptions.stripeCustomerId, customerId)); if (subData.status === "active" || subData.status === "trialing") { // Enable paid tier quotas and browser crawling await trx .update(users) .set({ bookmarkQuota: serverConfig.quotas.paid.bookmarkLimit, storageQuota: serverConfig.quotas.paid.assetSizeBytes, browserCrawlingEnabled: serverConfig.quotas.paid.browserCrawlingEnabled, }) .where(eq(users.id, existingSubscription.userId)); } else { // Set free tier quotas and disable browser crawling await trx .update(users) .set({ bookmarkQuota: serverConfig.quotas.free.bookmarkLimit, storageQuota: serverConfig.quotas.free.assetSizeBytes, browserCrawlingEnabled: serverConfig.quotas.free.browserCrawlingEnabled, }) .where(eq(users.id, existingSubscription.userId)); } }); return subData; } catch (error) { console.error("Error syncing Stripe data:", error); throw error; } } async function processEvent(event: Stripe.Event, db: Context["db"]) { if (!allowedEvents.includes(event.type)) { return; } const { customer: customerId } = event.data.object as { customer: string; }; if (typeof customerId !== "string") { throw new Error( `[STRIPE HOOK] Customer ID isn't string. Event type: ${event.type}`, ); } return await syncStripeDataToDatabase(customerId, db); } export const subscriptionsRouter = router({ getSubscriptionStatus: authedProcedure.query(async ({ ctx }) => { const subscription = await ctx.db.query.subscriptions.findFirst({ where: eq(subscriptions.userId, ctx.user.id), }); if (!subscription) { return { tier: "free" as const, status: null, startDate: null, endDate: null, hasActiveSubscription: false, cancelAtPeriodEnd: false, }; } return { tier: subscription.tier, status: subscription.status, startDate: subscription.startDate, endDate: subscription.endDate, hasActiveSubscription: subscription.status === "active" || subscription.status === "trialing", cancelAtPeriodEnd: subscription.cancelAtPeriodEnd || false, }; }), getSubscriptionPrice: authedProcedure.query(async () => { if (!stripe) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Stripe is not configured. Please contact your administrator.", }); } const { priceId } = requireStripeConfig(); const price = await stripe.prices.retrieve(priceId); return { priceId: price.id, currency: price.currency, amount: price.unit_amount, }; }), createCheckoutSession: authedProcedure.mutation(async ({ ctx }) => { const { stripe, priceId } = requireStripeConfig(); const user = await ctx.db.query.users.findFirst({ where: eq(users.id, ctx.user.id), columns: { email: true, }, with: { subscription: true, }, }); if (!user) { throw new TRPCError({ code: "NOT_FOUND", message: "User not found", }); } const existingSubscription = user.subscription; if (existingSubscription?.status === "active") { throw new TRPCError({ code: "BAD_REQUEST", message: "User already has an active subscription", }); } let customerId = existingSubscription?.stripeCustomerId; if (!customerId) { const customer = await stripe.customers.create({ email: user.email, metadata: { userId: ctx.user.id, }, }); customerId = customer.id; if (!existingSubscription) { await ctx.db.insert(subscriptions).values({ userId: ctx.user.id, stripeCustomerId: customerId, status: "unpaid", }); } else { await ctx.db .update(subscriptions) .set({ stripeCustomerId: customerId }) .where(eq(subscriptions.userId, ctx.user.id)); } } const session = await stripe.checkout.sessions.create({ customer: customerId, payment_method_types: ["card"], line_items: [ { price: priceId, quantity: 1, }, ], mode: "subscription", success_url: `${serverConfig.publicUrl}/settings/subscription?success=true`, cancel_url: `${serverConfig.publicUrl}/settings/subscription?canceled=true`, metadata: { userId: ctx.user.id, }, automatic_tax: { enabled: true, }, customer_update: { address: "auto", }, allow_promotion_codes: true, }); return { sessionId: session.id, url: session.url, }; }), syncWithStripe: authedProcedure.mutation(async ({ ctx }) => { const subscription = await ctx.db.query.subscriptions.findFirst({ where: eq(subscriptions.userId, ctx.user.id), }); if (!subscription?.stripeCustomerId) { // No Stripe customer found for user return { success: true }; } await syncStripeDataToDatabase(subscription.stripeCustomerId, ctx.db); return { success: true }; }), createPortalSession: authedProcedure.mutation(async ({ ctx }) => { const { stripe } = requireStripeConfig(); const subscription = await ctx.db.query.subscriptions.findFirst({ where: eq(subscriptions.userId, ctx.user.id), }); if (!subscription?.stripeCustomerId) { throw new TRPCError({ code: "BAD_REQUEST", message: "No Stripe customer found", }); } const session = await stripe.billingPortal.sessions.create({ customer: subscription.stripeCustomerId, return_url: `${serverConfig.publicUrl}/settings/subscription`, }); return { url: session.url, }; }), getQuotaUsage: authedProcedure.query(async ({ ctx }) => { const user = await ctx.db.query.users.findFirst({ where: eq(users.id, ctx.user.id), columns: { bookmarkQuota: true, storageQuota: true, }, }); if (!user) { throw new TRPCError({ code: "NOT_FOUND", message: "User not found", }); } // Get current bookmark count const [{ bookmarkCount }] = await ctx.db .select({ bookmarkCount: count() }) .from(bookmarks) .where(eq(bookmarks.userId, ctx.user.id)); // Get current storage usage const [{ storageUsed }] = await ctx.db .select({ storageUsed: sum(assets.size) }) .from(assets) .where(eq(assets.userId, ctx.user.id)); return { bookmarks: { used: bookmarkCount, quota: user.bookmarkQuota, unlimited: user.bookmarkQuota === null, }, storage: { used: Number(storageUsed) || 0, quota: user.storageQuota, unlimited: user.storageQuota === null, }, }; }), handleWebhook: publicProcedure .input( z.object({ body: z.string(), signature: z.string(), }), ) .mutation(async ({ input, ctx }) => { if (!stripe || !serverConfig.stripe.webhookSecret) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Stripe is not configured", }); } let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( input.body, input.signature, serverConfig.stripe.webhookSecret, ); } catch (err) { console.error("Webhook signature verification failed:", err); throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid signature", }); } try { await processEvent(event, ctx.db); return { received: true }; } catch (error) { console.error("Error processing webhook:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Internal server error", }); } }), });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/karakeep-app/karakeep'

If you have feedback or need assistance with the MCP directory API, please join our Discord server