DEV Community

Cover image for I Built an AI-Powered Education Platform as a Solo Founder - Here's My Full Stack
jackhan312
jackhan312

Posted on

I Built an AI-Powered Education Platform as a Solo Founder - Here's My Full Stack

I Built an AI-Powered Education Platform as a Solo Founder - Here's My Full Stack

After 6 months of nights and weekends, I shipped TopoForest - an AI-driven business education platform for indie builders going global. No co-founders, no VC funding, no team. Just me, a keyboard, and an unhealthy amount of coffee.

If you're a solo dev wondering whether you can actually pull off a SaaS by yourself, the answer is yes - but you'll need to be surgical about your tech choices. Here's the full stack I used, the trade-offs I made, and what I'd do differently.


The Big Picture

TopoForest is a learning platform that teaches OPC builders (solo entrepreneurs running a one-person business) how to leverage AI in their daily operations. It has video courses, user accounts, phone verification, and WeChat integration. All of it needs to work in China, which means dealing with the Chinese internet ecosystem - a whole different beast.

Here's the architecture at a glance:

+--------------------------------+-----------------------------+
|  Next.js + Koa (Vercel)        |  PostgreSQL (Prisma)        |
+--------------------------------+-----------------------------+
                |                                               
                v                                               
+-------------------------------------------------------------+
|  Aliyun OSS (Video/Image)                                  |
|  WeChat OAuth + Aliyun SMS                                 |
+-------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Simple enough, right? Let me walk through each piece.


Next.js (App Router) - The Frontend That Does Double Duty

I picked Next.js for two reasons: it's boring technology (in the best way), and the App Router handles both frontend and API routes in one codebase.

Why App Router over Pages Router?

I started with Pages Router, but App Router won me over with:

  • Server Components - zero client-side JavaScript for static content pages. The course catalog renders entirely on the server, which means my Lighthouse scores are stupidly good.
  • Layout nesting - I have a shared layout for authenticated pages (dashboard, courses, account) and a different one for public pages. No more wrapping every page in a HOC.
  • Route handlers - I use route.ts files for lightweight API endpoints that don't need the full backend treatment.

The real-world trade-off

App Router is not all sunshine. The caching behavior is confusing at first - I spent a full evening debugging why a page kept showing stale data. Turns out I needed to call revalidatePath() in the right place. Once you internalize the mental model, it's fine, but the learning curve is real.

// Example: revalidating after a course progress update
import { revalidatePath } from 'next/cache';

export async function updateProgress(courseId: string, data: ProgressData) {
  await prisma.courseProgress.upsert({
    where: { userId_courseId: { userId: data.userId, courseId } },
    update: data,
    create: { ...data, courseId },
  });
  revalidatePath(`/courses/${courseId}`);
}
Enter fullscreen mode Exit fullscreen mode

Verdict: Worth it. The DX is great once you're past the initial hump.


Koa API Backend - Why Not Just Next.js API Routes?

"Why a separate backend?" - this is the question I get asked most.

The answer: WeChat OAuth and ecosystem integration.

Next.js API routes are fantastic for simple CRUD, but when you're dealing with WeChat's callback mechanism, token exchange flows, and the need to track redirect statistics (click-through rates, visit sources, user agents), a dedicated backend framework keeps your codebase organized. I chose Koa for the API layer while keeping the frontend on Next.js - both running on Vercel.

Why Koa over Express?

Honest answer: I just like Koa better. The middleware is cleaner - no callback hell, proper async/await from the ground up. Express middleware often feels like you're fighting the framework. Koa gets out of your way.

// Koa middleware for tracking redirect visits
async function trackRedirect(ctx: Context, next: Next) {
  const { source, menuKey, openid } = ctx.query;

  await prisma.trackVisit.create({
    data: {
      source: source as string,
      menuKey: menuKey as string,
      openid: openid as string,
      ip: ctx.ip,
      userAgent: ctx.headers['user-agent'],
      referer: ctx.headers.referer,
    },
  });

  await next();
}
Enter fullscreen mode Exit fullscreen mode

A lesson learned the hard way

I initially tried using koa-connect to wrap Express middleware in Koa. Don't do this. It caused mysterious context leaks that took me a full weekend to track down. Write native Koa middleware. Yes, it's more work upfront, but you'll thank yourself later.

Verdict: If you don't need WeChat-level ecosystem integration, Next.js API routes alone would be fine. But for my use case, Koa was the right call.


Prisma + PostgreSQL - The Database Layer I Actually Enjoy

I've used raw SQL, Sequelize, TypeORM, and Drizzle. Prisma is the first ORM I actively enjoy using.

Why Prisma?

  • Type safety end-to-end - I define my schema, run prisma db push, and instantly get fully typed queries. My IDE autocompletes every field. I haven't written a misspelled column name in months.
  • Schema as documentation - the schema.prisma file is a single source of truth. When I onboard a contributor (someday), they can read one file and understand the entire data model.
  • Relations are trivial - nested writes are a one-liner. No more manual transaction management for related records.

Soft delete pattern

One rule I set early: never physically delete data. Every table has an enabled boolean field. Deletion is just setting enabled = false.

model Course {
  id        Int      @id @default(autoincrement())
  title     String
  enabled   Boolean  @default(true)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
Enter fullscreen mode Exit fullscreen mode

This means every query needs a where: { enabled: true } filter. It's a minor annoyance, but it's saved me multiple times when a user accidentally deleted something and I could just flip a flag back.

Why db push instead of migrate?

I use prisma db push to sync the schema. No migration files, no versioning headaches. For a solo developer shipping fast, migration files are overhead I don't need. If I ever bring on a team, I'll switch to proper migrations, but for now, db push keeps me fast.

prisma db push  # sync schema, no migration files
Enter fullscreen mode Exit fullscreen mode

Verdict: Prisma is a joy. The autocomplete alone is worth the price of admission (which is free, so that's a pretty good deal).


WeChat OAuth (snsapi_base) - The Hardest Part by Far

WeChat is the gateway to the Chinese internet. If your product targets Chinese users, you can't skip WeChat integration. But it's also the most painful part of the stack.

What I built

Users click a WeChat menu link ? they hit my backend ? I redirect to WeChat's OAuth page ? WeChat sends back a code ? I exchange it for an openid ? I redirect the user to the actual content page.

All of this happens silently (snsapi_base scope), so the user doesn't see an authorization popup. It's fast, it's seamless, and it took me a week to get right.

The gotchas

  1. WeChat's in-app browser just works - I was initially worried about compatibility quirks, but the built-in browser handles modern web apps without issues.
  2. You need a registered domain - WeChat won't accept IP addresses, and the domain must be registered with China's internet authority (ICP filing). Mine is topforest.cn.
  3. The state parameter is your friend - I pass state=home, state=courses, or state=account to track which menu item the user clicked. This feeds into my analytics.

The redirect tracking middleware

Every WeChat menu click goes through a tracking endpoint. I log the source, menu key, and openid, then issue a 302 redirect:

// Simplified redirect handler
router.get('/api/v1/track/redirect', trackRedirect, async (ctx) => {
  const { state } = ctx.query;
  const targetUrl = getTargetUrl(state as string);
  ctx.status = 302;
  ctx.redirect(targetUrl);
});
Enter fullscreen mode Exit fullscreen mode

Verdict: The hardest part of the stack, but also the most valuable. WeChat integration is a moat - competitors can't just copy it.


Aliyun OSS - Video Storage That Doesn't Break the Bank

I host video courses on Aliyun OSS (Object Storage Service). It's cheap, fast, and integrates well with the rest of the Alibaba Cloud ecosystem.

Signed URLs

Videos are private by default. When a user requests a course video, my backend generates a signed URL with a time-limited expiration. This prevents hotlinking and keeps my bandwidth costs under control.

import OSS from 'ali-oss';

const client = new OSS({
  region: 'oss-cn-hangzhou',
  accessKeyId: process.env.OSS_ACCESS_KEY_ID!,
  accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!,
  bucket: process.env.OSS_BUCKET!,
});

async function getSignedUrl(objectKey: string): Promise<string> {
  return client.signatureUrl(objectKey, { expires: 3600 }); // 1 hour
}
Enter fullscreen mode Exit fullscreen mode

A painful lesson

When I first deployed, videos were returning 404 errors. I was confused for hours. Turns out I had forgotten to set the OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET, and OSS_BUCKET environment variables. Without them, OSS was returning unsigned URLs - which means public access is denied. Now I have a startup check that validates all three exist before the server starts.

Verdict: Great value, but triple-check your env vars. The error messages are not helpful.


Aliyun SMS - Phone Verification the Chinese Way

Phone verification is standard in China, not email. I use Aliyun SMS for sending verification codes.

The setup

You need to configure a template in the Aliyun SMS console, then reference it in your code:

import Dysmsapi from '@alicloud/dysmsapi20170525';

const client = new Dysmsapi({
  accessKeyId: process.env.ALIYUN_SMS_ACCESS_KEY_ID!,
  accessKeySecret: process.env.ALIYUN_SMS_ACCESS_KEY_SECRET!,
});

async function sendVerificationCode(phone: string, code: string) {
  await client.sendSms({
    phoneNumbers: phone,
    signName: process.env.ALIYUN_SMS_SIGN_NAME!,
    templateCode: process.env.ALIYUN_SMS_TEMPLATE_CODE!,
    templateParam: JSON.stringify({ code }),
  });
}
Enter fullscreen mode Exit fullscreen mode

The gotcha

The template code and sign name must match exactly what you configured in the Aliyun console. If either is wrong, the API returns a cryptic "template not found" error. I wasted half a day on this.

Verdict: Works well once configured, but the setup is finicky. Double-check every string.


Vercel Deployment - Everything Lives Here

I deploy both the frontend and the Koa backend on Vercel. The frontend uses Vercel's native Next.js hosting, and the Koa API runs as a serverless function on the same Vercel project - no separate server, no dual-infrastructure headache.

The setup

  • Frontend: Next.js App Router, deployed natively on Vercel with a custom domain (topforest.cn)
  • Backend: Koa wrapped as a Vercel serverless function, handling API routes under /api/v1
  • Environment: NEXT_PUBLIC_BACKEND_URL=https://topforest.cn in Vercel's environment variables
  • Database: PostgreSQL via Vercel Postgres (or any compatible Postgres provider)

Why Vercel?

I don't want to think about infrastructure. Vercel handles SSL, CDN, and the deployment pipeline. I push to main, and it's live in 2 minutes. For a solo developer, that's priceless.

Keeping everything under one roof means:

  • One deployment pipeline - no syncing frontend and backend releases
  • One set of environment variables - no duplication across servers
  • One bill - predictable pricing, no surprise compute charges

Verdict: If you're a solo dev, put everything on Vercel. You have better things to do than configure nginx.


What I'd Do Differently

No project is perfect. Here's what I'd change if I were starting over:

  1. Add monitoring earlier - I shipped without proper error tracking, and I only found out about a video upload failure because a user emailed me. I've since added Sentry (which integrates beautifully with Vercel), but I should have done it from day one.
  2. Test WeChat OAuth earlier - I built the entire course system before testing the OAuth flow. When it broke, I had to rework the authentication layer. Test the hardest integration first.
  3. Use a monorepo from the start - My frontend and backend are in separate repos. Sharing types between them is a pain. I'd use Turborepo or Nx if I were starting fresh.
  4. Write fewer features, test more - I shipped too many features too quickly. Some of the early features barely get used. I should have focused on the core loop and iterated.

The Numbers (So Far)

I shipped this as a solo founder. Here's what that looks like in practice:

  • 6 months of nights and weekends
  • ~45,000 lines of code across frontend and backend
  • 1 person (me) doing design, development, deployment, and content
  • 0 VC funding - bootstrapped entirely

Is it profitable yet? Not yet. But it's shipping, it's getting users, and I'm learning more than any tutorial could teach me.


Should You Build Your SaaS?

If you're on the fence about building your own SaaS as a solo developer, here's my honest take:

The tech stack is not the hard part. Next.js, Prisma, PostgreSQL - these are all well-documented, battle-tested tools. The hard part is everything else: figuring out what to build, staying motivated when nobody's using it, and shipping through the discomfort of "this isn't ready yet."

But if you can push through that, the tech side is genuinely fun. There's nothing quite like watching a user sign up for something you built entirely by yourself.


What's Next?

I'm currently working on:

  • AI-powered course recommendations - using user behavior to suggest the next course
  • Internationalization - adding English support for non-Chinese users
  • Analytics dashboard - giving users visibility into their learning progress

If any of this resonates, I'd love to hear your story. What's your tech stack? What did you build? Drop a comment - I read every single one.


You can check out TopoForest at topforest.cn. I'm also on X (Twitter) sharing my build-in-public journey - come say hi!

Top comments (0)