Lingo is a Duolingo-style language app built with Next.js, Clerk and Drizzle
Lingo - Interactive platform for language learning.
At a glance
- What is it?
- Lingo is an open source language learning platform that clones the Duolingo loop of lessons, hearts, quests and streaks, and it is unusually complete for a clone: it has server actions, a Stripe subscription tier, a Clerk-based admin panel backed by react-admin, and a seed script that writes the challenge content into Postgres. The deployment target is Vercel with a Neon database.
- Who is it for?
- Adopt Lingo as a starting point if you want a language learning web app with the engagement mechanics already built, since lessons, hearts, streaks, quests, a subscription tier and an admin panel are all there. Skip it if you want a mobile app, since this is a Next.js web application, and if you need content you did not write yourself, because the challenge content comes from the seed script.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The engagement mechanics are the product
Lingo is an interactive platform for language learning, and the description of the app is short, so the interesting part is what the code reveals about what it does.
The folder structure shows the shape immediately, and it is the shape of a Duolingo clone done properly. There is an actions/ directory with three server action files, challenge-progress.ts, user-progress.ts and user-subscription.ts. There is a store/ directory with three Zustand-style hooks for modals, use-exit-modal.ts, use-hearts-modal.ts and use-practice-modal.ts. And there is a components/ directory with the sidebar, a feed wrapper, a banner, a promo panel, quests and a user-progress component.
So the loop is visible in the file names. A user does a challenge, progress is recorded on the server, hearts are a limited resource that runs out, and there is a quest or streak concept on top. The practice modal hook is the one that suggests an actual lesson player lives in the app/ route tree rather than in components, which is the usual split for this kind of app.
The components/ directory also has modals/ and ui/ subdirectories, and the presence of components.json at the root tells you the UI primitives follow the shadcn/ui convention, with radix-ui packages in the dependency list to back that up.
The project is TypeScript, MIT licensed, with a live deployment at lingo-clone.vercel.app. The last push was on 2026-10-15 and there are no GitHub releases, so the repository is the distribution route.
Next.js 16 with Turbopack for both dev and build
The package file shows the stack, and the scripts show the current state of the framework.
The application is Next.js at 16.4.0 with React at 19.3.0, and the dev and build scripts both use Turbopack:
"dev": "next dev --turbopack",
"build": "next build --turbopack",That is worth noting, because Turbopack was for a long time the dev-only bundler and is now doing the production build too. For a project on Next 16, that is the default rather than a choice, but it does mean the build behaviour is not the webpack behaviour most people have in their heads.
The rest of the scripts are conventional and reveal the tooling. Lint and lint:fix run eslint, format and format:fix run prettier, and there are three database scripts. `db:studio` opens drizzle-kit studio, which is the database browser. `db:push` runs drizzle-kit push, which applies the schema to the database. And `db:prod` runs `tsx ./scripts/prod.ts`, the seed script.
The start script is `next start`, and the package is private at version 1.0.1, with pnpm as the package manager and a pnpm-workspace.yaml in the tree alongside the lock file.
The styling stack is Tailwind, with tailwind.config.ts at the root, postcss.config.js, and clsx and tailwind-merge for class composition. The icons are lucide-react, the toasts are sonner, and react-confetti provides the celebration effect that these apps always end up needing.
There is a components.json file, which is the shadcn/ui configuration file, so the ui/ directory is where the shadcn primitives live.
Clerk handles auth and roles, and the admin panel is react-admin
The authentication and administration setup is the part of this clone that goes beyond the Duolingo mechanics, and it is worth reading on its own.
Clerk provides the authentication, with @clerk/nextjs at 7.9.10, and it is configured through the environment file with a publishable key and a secret key. The telemetry is disabled in the same file, with three separate flags, one for Next, one for Clerk and a general DO_NOT_TRACK.
The roles are handled with an environment variable rather than in the database. CLERK_ADMIN_IDS holds the Clerk user IDs of the administrators, separated by commas, and the comment in the file shows the multi-admin form. The setup instructions describe the source as the Clerk Dashboard or Settings Page, and the procedure is to find the section related to admin user IDs and copy the IDs, ensuring they are separated by commas and spaces.
That is a simple and effective model. The admin list lives in configuration, so promoting someone is an environment change rather than a database update, and the alternative of a roles table in Postgres would have been more machinery for a feature that changes rarely.
The admin interface itself is react-admin at 5.15.4 with ra-data-simple-rest, which is the generic REST data provider. That is the notable choice here, because react-admin is a whole framework for admin panels, and using it means the admin side is a set of resource definitions over a REST API rather than a set of hand-built pages. The lib/admin.ts file in the tree is the logical place for that configuration.
The app/ directory has an admin/ route group, which is where that interface is served from.
Drizzle over a Neon Postgres database
The database layer is Drizzle ORM over a serverless Postgres, and the configuration is in the db/ directory.
There are three files. `db/schema.ts` holds the table definitions. `db/drizzle.ts` holds the client. And `db/queries.ts` holds the query functions, which is the separation that keeps SQL out of the components.
The provider is @neondatabase/serverless, which is Neon-specific, and the connection string in the environment file is a Postgres URI with the sslmode=require parameter. The setup steps are explicit about getting that right, telling you to replace the user, password, host and port placeholders and to include the query parameter for the SSL mode requirement.
Drizzle is the schema-first option, and it suits this project for a reason that is visible in the file list. The schema is written as TypeScript, so the types flow into the server actions and the components without a separate type generation step, and `drizzle-kit push` applies the schema directly to the database without a migration directory, which is what the db:push script is for.
The server actions in the actions/ directory then work against the database through those queries. The three files map onto the three kinds of state the app cares about: challenge progress, which is what a user has completed, user progress, which is the streak and the XP, and user subscription, which is the Stripe tier.
There is also a constants.ts at the root, which in this kind of app is usually the game economy, the heart maximum, the XP per lesson and the streak rules.
The seed script is what makes the app usable
The setup instructions have a step that decides whether the app works when you open it, and it is the step most clones skip.
After installing the dependencies with pnpm install, you run the seed script:
pnpm run db:push && pnpm run db:prodThe first part applies the schema, and the second runs the seed, which the instructions say uses tsx to execute the TypeScript file at scripts/prod.ts and writes the challenges data into the database.
So the lesson content is not in the repository as a static file that the app reads at request time, it is rows in the database that the seed script inserts. The app/lesson/ route and the challenge-progress server action both read from those rows.
That is the right design for a language app, because a lesson is a graph of challenges with answers and ordering, and once the number of lessons grows, a relational representation with a progress table is the only workable one. It also means the app is empty on a fresh clone until the seed has run, which is why the instructions include a verification step, checking the database once the script completes to make sure the challenge data is there.
The scripts/ directory holds that one file, prod.ts, and the .env/.env.local entry in the folder structure suggests the environment file is kept in an .env/ directory in this project rather than at the root, with the .env.example at the root as the template.
Stripe is the subscription layer, and there is a webhook for it
The monetisation side uses Stripe, and it is wired in the standard way with one addition worth noting.
The dependency is stripe at 22.6.2, and the lib/ directory holds a stripe.ts alongside admin.ts and utils.ts. The server action file user-subscription.ts is the read side, so the subscription state is fetched on the server rather than from the client.
The environment holds both the API secret key and a webhook secret. Having both is not redundant, and the distinction matters if you build on this. The API key is for calls you make to Stripe, and the webhook secret is for verifying the signature on the events Stripe sends back. A subscription tier is a piece of state that Stripe owns, so the app has to both read it and be told when it changes.
The other variables complete the deployment picture. NEXT_PUBLIC_APP_URL is the public URL of the application, used for the Stripe return URLs, and the setup instruction is to replace the localhost value with the URL of the deployed application. The @vercel/config dependency at 0.9.0 and the vercel.ts file at the root are there for the deployment target.
The repository also carries a proxy.ts file, which in a Next.js application is the middleware entry point, and it is where the Clerk authentication check and the admin route protection would be enforced.
Where the app would need work before anyone could use it
The honest evaluation is that this is a good demonstration of the mechanics and a thin product.
The evidence for the first claim is that everything the Duolingo loop needs is present and wired to a database. Hearts that deplete, practice modals, a feed wrapper, quests, a sidebar, a promo banner and a user progress view. Server actions for the three kinds of progress. An admin panel over a REST data provider, which means a non-developer can inspect and change the content. And a subscription tier, which means there is a business model attached rather than only a demo.
The gaps are in the content and in the polish. The challenges come from the seed script, and there is no sign of a content authoring pipeline, which for a real product is the hard part, since a language app lives or dies on having enough lessons and on them being good. The progress model is per user and per challenge, with no adaptive placement, no spaced repetition and no difficulty adjustment, which is what a serious competitor would do.
The other thing to weigh is the stack's weight. React-admin, Clerk, Stripe, Drizzle, Radix, Lucide, sonner, confetti and a circular progress bar is a lot of dependencies for a lesson player, and each one is a thing to keep current.
For a reader who wants to build something like this, the value is in the wiring rather than the design. The server action pattern, the Drizzle schema, the Clerk admin model, the Stripe webhook and the seed script are all readable in a repository this size, and they are the parts that are genuinely tedious to get right from scratch.
Editorial conclusion
Adopt Lingo as a starting point if you want a language learning web app with the engagement mechanics already built, since lessons, hearts, streaks, quests, a subscription tier and an admin panel are all there. Skip it if you want a mobile app, since this is a Next.js web application, and if you need content you did not write yourself, because the challenge content comes from the seed script. Verify first that you have a Clerk account with admin user IDs, a Neon or other Postgres database and a Stripe account with a webhook secret, since all three are required before the app is usable, and that you run the seed script before first use, because the lessons come from the database.
Frequently asked questions
How do I set up the Lingo clone locally?
Install Git and NodeJS, clone the repository, create a `.env.local` file in the root with your Clerk publishable and secret keys, a Postgres `DATABASE_URL` ending in `?sslmode=require`, a Stripe API key and webhook secret, `NEXT_PUBLIC_APP_URL` and `CLERK_ADMIN_IDS`, then run `pnpm install` and finally `pnpm run db:push && pnpm run db:prod` to apply the schema and seed the challenge data.
What framework and database does Lingo use?
Next.js 16.4.0 with React 19.3.0, and both the dev and build scripts use Turbopack. The database is Postgres through Drizzle ORM with the @neondatabase/serverless driver, the schema lives in db/schema.ts, and the queries in db/queries.ts are called from server actions.
How are administrators and payments handled?
Administrators are identified by Clerk user IDs listed in the CLERK_ADMIN_IDS environment variable, separated by commas, and the admin interface itself is built with react-admin and the ra-data-simple-rest provider. Payments use Stripe with both an API secret key and a webhook secret in the environment, and the subscription state is read on the server through the user-subscription server action.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sanidhyy-duolingo-clone)Was this useful?