1. YouTube Summaries
  2. Building a Full-Stack Code Execution Platform with Next.js, Convex, and Lemon Squeezy

Building a Full-Stack Code Execution Platform with Next.js, Convex, and Lemon Squeezy

By scribe 8 minute read

Create articles from any YouTube video or use our API to get YouTube transcriptions

Start for free
or, create a free article to see how easy it is.

In this comprehensive tutorial, we'll walk through building a full-stack code execution platform using Next.js, Convex, and Lemon Squeezy. This platform allows users to write, execute, and share code snippets in multiple programming languages. We'll cover everything from setting up the project to implementing key features and deploying the final application.

Project Setup

Let's start by setting up our Next.js project:

npx [email protected] .

We're using a specific version (15.0.3) to ensure compatibility throughout this tutorial.

Authentication with Clerk

For authentication, we'll use Clerk. First, install the necessary packages:

npm install @clerk/nextjs

Then, set up your Clerk application and add the required environment variables to your .env.local file:

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
CLERK_SECRET_KEY=your_secret_key

Create a middleware file to handle authentication:

// middleware.ts
import { authMiddleware } from "@clerk/nextjs";

export default authMiddleware();

export const config = {
  matcher: ["/((?!.*\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

Wrap your application with the ClerkProvider in your layout.tsx file:

import { ClerkProvider } from '@clerk/nextjs';

export default function RootLayout({ children }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

Setting up Convex

Convex will serve as our backend database and real-time data layer. Install Convex:

npm install convex

Initialize Convex in your project:

npx convex dev

This will create a convex folder and add necessary environment variables to your .env.local file.

Create a schema file in the convex folder to define your database tables:

// convex/schema.ts
import { defineSchema, defineTable } from "convex/schema";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    userId: v.string(),
    email: v.string(),
    name: v.string(),
    isPro: v.boolean(),
    proSince: v.optional(v.number()),
    lemonSqueezyCustomerId: v.optional(v.string()),
    lemonSqueezyOrderId: v.optional(v.string()),
  }).index("by_userId", ["userId"]),
  
  codeExecutions: defineTable({
    userId: v.string(),
    language: v.string(),
    code: v.string(),
    output: v.optional(v.string()),
    error: v.optional(v.string()),
  }).index("by_userId", ["userId"]),
  
  snippets: defineTable({
    userId: v.string(),
    username: v.string(),
    title: v.string(),
    language: v.string(),
    code: v.string(),
  }).index("by_userId", ["userId"]),
  
  comments: defineTable({
    snippetId: v.id("snippets"),
    userId: v.string(),
    username: v.string(),
    content: v.string(),
  }).index("by_snippetId", ["snippetId"]),
  
  stars: defineTable({
    userId: v.string(),
    snippetId: v.id("snippets"),
  })
    .index("by_userId", ["userId"])
    .index("by_snippetId", ["snippetId"])
    .index("by_userId_and_snippetId", ["userId", "snippetId"]),
});

Building the Code Editor

For our code editor, we'll use the Monaco Editor. Install the necessary packages:

npm install @monaco-editor/react

Create a custom hook to manage the editor state:

// hooks/useCodeEditor.ts
import { useState, useEffect } from 'react';
import { languageConfig } from '../constants';

export const useCodeEditor = () => {
  const [language, setLanguage] = useState('javascript');
  const [code, setCode] = useState('');
  const [output, setOutput] = useState('');
  const [isRunning, setIsRunning] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const savedCode = localStorage.getItem(`editorCode_${language}`);
    if (savedCode) {
      setCode(savedCode);
    } else {
      setCode(languageConfig[language].defaultCode);
    }
  }, [language]);

  const handleCodeChange = (newCode) => {
    setCode(newCode);
    localStorage.setItem(`editorCode_${language}`, newCode);
  };

  const handleLanguageChange = (newLanguage) => {
    setLanguage(newLanguage);
  };

  const runCode = async () => {
    setIsRunning(true);
    setError(null);
    setOutput('');

    try {
      // Implement code execution logic here
    } catch (err) {
      setError(err.message);
    } finally {
      setIsRunning(false);
    }
  };

  return {
    language,
    code,
    output,
    isRunning,
    error,
    setLanguage: handleLanguageChange,
    setCode: handleCodeChange,
    runCode,
  };
};

Create the EditorPanel component:

// components/EditorPanel.tsx
import { useCodeEditor } from '../hooks/useCodeEditor';
import { Editor } from '@monaco-editor/react';

export const EditorPanel = () => {
  const { language, code, setCode } = useCodeEditor();

  return (
    <div className="relative h-full">
      <Editor
        height="100%"
        language={language}
        value={code}
        onChange={setCode}
        theme="vs-dark"
        options={{
          minimap: { enabled: false },
          fontSize: 14,
          lineNumbers: 'on',
          scrollBeyondLastLine: false,
          automaticLayout: true,
        }}
      />
    </div>
  );
};

Implementing Code Execution

To execute code, we'll use the Piston API. Create a new file to handle code execution:

// lib/codeExecution.ts
import { languageConfig } from '../constants';

const PISTON_API_URL = 'https://emkc.org/api/v2/piston/execute';

export const executeCode = async (language, code) => {
  const runtime = languageConfig[language].pistonRuntime;

  const response = await fetch(PISTON_API_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      language: runtime.language,
      version: runtime.version,
      files: [{ content: code }],
    }),
  });

  const data = await response.json();

  if (data.message) {
    throw new Error(data.message);
  }

  if (data.compile && data.compile.code !== 0) {
    throw new Error(data.compile.stderr || data.compile.output);
  }

  if (data.run.code !== 0) {
    throw new Error(data.run.stderr || data.run.output);
  }

  return data.run.output.trim();
};

Update the runCode function in the useCodeEditor hook to use this execution function:

const runCode = async () => {
  setIsRunning(true);
  setError(null);
  setOutput('');

  try {
    const result = await executeCode(language, code);
    setOutput(result);
  } catch (err) {
    setError(err.message);
  } finally {
    setIsRunning(false);
  }
};

Building the Snippets Feature

Create a new file for snippet-related functions:

// convex/snippets.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

export const createSnippet = mutation({
  args: {
    title: v.string(),
    language: v.string(),
    code: v.string(),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw new Error("Not authenticated");
    }

    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("userId"), identity.subject))
      .first();

    if (!user) {
      throw new Error("User not found");
    }

    const snippet = await ctx.db.insert("snippets", {
      userId: identity.subject,
      username: user.name,
      title: args.title,
      language: args.language,
      code: args.code,
    });

    return snippet;
  },
});

export const getSnippets = query({
  handler: async (ctx) => {
    return await ctx.db.query("snippets").order("desc").collect();
  },
});

export const getSnippetById = query({
  args: { id: v.id("snippets") },
  handler: async (ctx, args) => {
    return await ctx.db.get(args.id);
  },
});

Create a SnippetCard component to display snippets:

// components/SnippetCard.tsx
import Link from 'next/link';
import { formatDistanceToNow } from 'date-fns';
import { StarButton } from './StarButton';

export const SnippetCard = ({ snippet }) => {
  return (
    <div className="bg-gray-800 rounded-lg p-4 shadow-md">
      <div className="flex justify-between items-center mb-2">
        <h3 className="text-lg font-semibold text-white">{snippet.title}</h3>
        <StarButton snippetId={snippet._id} />
      </div>
      <p className="text-gray-400 text-sm mb-2">
        by {snippet.username} • {formatDistanceToNow(new Date(snippet._creationTime))} ago
      </p>
      <div className="bg-gray-900 rounded p-2 mb-2">
        <code className="text-sm text-gray-300">{snippet.code.slice(0, 100)}...</code>
      </div>
      <Link href={`/snippets/${snippet._id}`} className="text-blue-400 hover:text-blue-300 text-sm">
        View full snippet
      </Link>
    </div>
  );
};

Implementing the Star Feature

Create functions for starring and unstarring snippets:

// convex/snippets.ts
export const starSnippet = mutation({
  args: { snippetId: v.id("snippets") },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw new Error("Not authenticated");
    }

    const existingStar = await ctx.db
      .query("stars")
      .filter((q) =>
        q.and(
          q.eq(q.field("userId"), identity.subject),
          q.eq(q.field("snippetId"), args.snippetId)
        )
      )
      .first();

    if (existingStar) {
      await ctx.db.delete(existingStar._id);
    } else {
      await ctx.db.insert("stars", {
        userId: identity.subject,
        snippetId: args.snippetId,
      });
    }
  },
});

export const getStarCount = query({
  args: { snippetId: v.id("snippets") },
  handler: async (ctx, args) => {
    const stars = await ctx.db
      .query("stars")
      .filter((q) => q.eq(q.field("snippetId"), args.snippetId))
      .collect();
    return stars.length;
  },
});

Create a StarButton component:

// components/StarButton.tsx
import { useState } from 'react';
import { useMutation, useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';

export const StarButton = ({ snippetId }) => {
  const starCount = useQuery(api.snippets.getStarCount, { snippetId });
  const star = useMutation(api.snippets.starSnippet);
  const [isStarred, setIsStarred] = useState(false);

  const handleStar = async () => {
    await star({ snippetId });
    setIsStarred(!isStarred);
  };

  return (
    <button
      onClick={handleStar}
      className={`flex items-center space-x-1 ${isStarred ? 'text-yellow-400' : 'text-gray-400'}`}
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        className="h-5 w-5"
        viewBox="0 0 20 20"
        fill="currentColor"
      >
        <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
      </svg>
      <span>{starCount}</span>
    </button>
  );
};

Setting up Lemon Squeezy for Payments

To handle payments, we'll use Lemon Squeezy. First, create an account and set up a product for your Pro plan.

Install the Lemon Squeezy SDK:

npm install lemonsqueezy.ts

Create a new file to handle Lemon Squeezy interactions:

// lib/lemonSqueezy.ts
import { LemonSqueezy } from 'lemonsqueezy.ts';

const ls = new LemonSqueezy(process.env.LEMON_SQUEEZY_API_KEY);

export const createCheckout = async (userEmail) => {
  const checkout = await ls.createCheckout({
    storeId: process.env.LEMON_SQUEEZY_STORE_ID,
    variantId: process.env.LEMON_SQUEEZY_VARIANT_ID,
    customData: { userEmail },
  });

  return checkout.data.attributes.url;
};

Create a webhook handler to process successful payments:

// pages/api/lemon-squeezy-webhook.js
import { buffer } from 'micro';
import { verifyWebhook } from 'lemonsqueezy.ts';
import { updateUserToPro } from '../../lib/users';

export const config = {
  api: {
    bodyParser: false,
  },
};

export default async function handler(req, res) {
  if (req.method === 'POST') {
    const buf = await buffer(req);
    const signature = req.headers['x-signature'];

    try {
      const event = verifyWebhook(buf, signature, process.env.LEMON_SQUEEZY_WEBHOOK_SECRET);

      if (event.type === 'order_created') {
        const { user_email } = event.data.attributes.custom_data;
        await updateUserToPro(user_email);
      }

      res.status(200).json({ message: 'Webhook processed successfully' });
    } catch (err) {
      console.error('Webhook error:', err);
      res.status(400).json({ message: 'Webhook verification failed' });
    }
  } else {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
  }
}

Update the user's status to Pro:

// lib/users.ts
import { api } from '../convex/_generated/api';

export const updateUserToPro = async (email) => {
  await api.users.updateToPro({ email });
};

Building the Profile Page

Create a profile page to display user information and their snippets:

// pages/profile.tsx
import { useUser } from '@clerk/nextjs';
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
import { SnippetCard } from '../components/SnippetCard';

export default function ProfilePage() {
  const { user } = useUser();
  const snippets = useQuery(api.snippets.getUserSnippets, { userId: user?.id });

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Your Profile</h1>
      <div className="mb-8">
        <h2 className="text-2xl font-semibold mb-4">Your Snippets</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
          {snippets?.map((snippet) => (
            <SnippetCard key={snippet._id} snippet={snippet} />
          ))}
        </div>
      </div>
    </div>
  );
}

Deploying the Application

To deploy your application, you can use Vercel. First, push your code to a GitHub repository.

Then, follow these steps:

  1. Sign up for a Vercel account and connect it to your GitHub account.
  2. Create a new project and select your repository.
  3. Configure your environment variables in the Vercel dashboard.
  4. Deploy your application.

Make sure to update your Clerk and Convex configurations with the production URL of your deployed application.

Conclusion

In this tutorial, we've built a full-stack code execution platform using Next.js, Convex, and Lemon Squeezy. We've implemented key features such as code execution, snippet sharing, and a Pro plan with payments. This application demonstrates how to combine modern web technologies to create a powerful and scalable platform.

To further improve the application, consider adding features like:

  • User profiles with avatars
  • Commenting on snippets
  • Tagging snippets for better organization
  • Advanced code editor features (e.g., multiple files, themes)
  • Integration with version control systems

Remember to always follow best practices for security, performance, and user experience as you continue to develop and maintain your application.

Article created from: https://youtu.be/fGkRQgf6Scw?si=DHiRt7SWZNkcf2hP

Ready to automate your
LinkedIn, Twitter and blog posts with AI?

Start for free