<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: plainform</title>
    <description>The latest articles on DEV Community by plainform (@plainform).</description>
    <link>https://dev.to/plainform</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3901310%2F9cfba6c5-af5c-4dd9-849e-fce591aa5e83.png</url>
      <title>DEV Community: plainform</title>
      <link>https://dev.to/plainform</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/plainform"/>
    <language>en</language>
    <item>
      <title>How Authentication Works in Plainform</title>
      <dc:creator>plainform</dc:creator>
      <pubDate>Fri, 05 Jun 2026 18:47:48 +0000</pubDate>
      <link>https://dev.to/plainform/how-authentication-works-in-plainform-3kk5</link>
      <guid>https://dev.to/plainform/how-authentication-works-in-plainform-3kk5</guid>
      <description>&lt;p&gt;Authentication is one of those things that looks simple from the outside and turns into a multi-week project the moment you start building it properly. Sign-in, sign-up, email verification, password reset, OAuth, route protection — each piece is straightforward on its own, but wiring them all together correctly takes time and care.&lt;/p&gt;

&lt;p&gt;Plainform ships with all of it already done. This post walks through exactly how it works, from the moment a user lands on the sign-up page to the moment they are authenticated and using your app.&lt;/p&gt;

&lt;p&gt;Plainform Auth Stack&lt;br&gt;
Plainform uses Clerk for identity, custom React forms for the user interface, middleware for route protection, and server-side checks for sensitive pages and actions. The result is a production-ready authentication flow without requiring you to build sessions, email verification, OAuth, or password reset from scratch.&lt;/p&gt;

&lt;p&gt;The flow is intentionally split between Clerk and the application:&lt;/p&gt;

&lt;p&gt;Clerk owns identity, sessions, email verification, OAuth, and password recovery.&lt;br&gt;
Plainform owns the custom sign-in and sign-up UI.&lt;br&gt;
Middleware protects routes before protected pages render.&lt;br&gt;
Server-side checks protect sensitive actions and order-specific pages.&lt;br&gt;
The Foundation: Clerk&lt;br&gt;
Plainform uses Clerk as its authentication provider. Clerk is a hosted identity platform that handles sessions, tokens, OAuth flows, and user management. The reason it is the right choice here is not just convenience — it is that authentication is a security-critical system, and delegating it to a service that specializes in it is the pragmatic call.&lt;/p&gt;

&lt;p&gt;All the auth logic in Plainform is built on top of Clerk's React hooks and server-side helpers. The forms are custom, the UI is Plainform's own, but the underlying session management and identity verification is Clerk's.&lt;/p&gt;

&lt;p&gt;One thing that makes Clerk particularly well-suited for Next.js projects is how naturally it fits into the App Router model. Server components, client components, middleware, and API routes all have first-class access to the current user's session through consistent, well-documented APIs. There is no context juggling or custom session providers to set up. You call auth() on the server or useAuth() on the client and you get what you need.&lt;/p&gt;

&lt;p&gt;Sign-Up Flow&lt;br&gt;
When a new user signs up, they go through two steps: creating their account and verifying their email address.&lt;/p&gt;

&lt;p&gt;Step 1: Account creation&lt;br&gt;
The SignUpForm component handles the initial form. It collects an email address and password, validates them client-side with Zod before anything hits the network, and then calls Clerk's signUp.create method.&lt;/p&gt;

&lt;p&gt;The password rules are enforced via a shared Zod schema:&lt;/p&gt;

&lt;p&gt;export const signUpSchema = z.object({&lt;br&gt;
  email_address: z.string().email().toLowerCase().trim(),&lt;br&gt;
  password: z&lt;br&gt;
    .string()&lt;br&gt;
    .min(8)&lt;br&gt;
    .regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })&lt;br&gt;
    .regex(/[0-9]/, { message: 'Contain at least one number.' })&lt;br&gt;
    .regex(/[^a-zA-Z0-9]/, { message: 'Contain at least one special character.' }),&lt;br&gt;
});&lt;br&gt;
Once Clerk accepts the account creation, it immediately triggers email verification:&lt;/p&gt;

&lt;p&gt;await signUp.create({ emailAddress: email_address, password });&lt;br&gt;
await signUp.prepareEmailAddressVerification({ strategy: 'email_code' });&lt;br&gt;
setVerifying(true);&lt;br&gt;
The component then swaps itself out for the verification form without any page navigation.&lt;/p&gt;

&lt;p&gt;Step 2: Email verification&lt;br&gt;
The SignUpVerificationForm renders a six-digit OTP input. The user enters the code from their email, and the form calls signUp.attemptEmailAddressVerification. If the code is correct and the sign-up is complete, Clerk returns a session ID and the user is activated:&lt;/p&gt;

&lt;p&gt;const signUpAttempt = await signUp.attemptEmailAddressVerification({ code });&lt;br&gt;
if (signUpAttempt.status === 'complete') {&lt;br&gt;
  await setActive({ session: signUpAttempt.createdSessionId });&lt;br&gt;
  if (signUpAttempt.emailAddress) {&lt;br&gt;
    await addContact(signUpAttempt.emailAddress); // adds to newsletter list&lt;br&gt;
  }&lt;br&gt;
  router.push('/');&lt;br&gt;
}&lt;br&gt;
One detail worth noting: after a successful sign-up, the user's email is automatically sent through the newsletter helper and added to Mailchimp. This is the newsletter subscription hook, and it happens silently as part of the auth flow.&lt;/p&gt;

&lt;p&gt;The verification form also includes a resend timer. Users have to wait 60 seconds before requesting a new code, which is enforced client-side with a countdown and a disabled resend button.&lt;/p&gt;

&lt;p&gt;This two-step flow is worth the extra friction. Verifying the email address up front means you are not accumulating unverified accounts in your user list, and it confirms that the person signing up actually controls the email they provided. It also gives Clerk enough signal to establish a trusted identity before issuing a session.&lt;/p&gt;

&lt;p&gt;Sign-In Flow&lt;br&gt;
Sign-in is more straightforward. The SignInForm collects an email and password, validates with the same Zod schema pattern, and calls signIn.create:&lt;/p&gt;

&lt;p&gt;const signInAttempt = await signIn.create({ identifier, password });&lt;br&gt;
if (signInAttempt.status === 'complete') {&lt;br&gt;
  await setActive({ session: signInAttempt.createdSessionId });&lt;br&gt;
  router.push('/');&lt;br&gt;
  window.location.reload();&lt;br&gt;
}&lt;br&gt;
The window.location.reload() after the redirect is intentional. It ensures that any server components that depend on the session state are re-rendered with the fresh session rather than serving stale cached output.&lt;/p&gt;

&lt;p&gt;The form also supports being rendered inside a modal via an isInModal prop. When in modal mode, it skips the router redirect and just reloads the page in place.&lt;/p&gt;

&lt;p&gt;OAuth: Google and GitHub&lt;br&gt;
Both sign-in and sign-up support OAuth via Google and GitHub. The OAuthConnection component handles the flow with Clerk's redirect-based OAuth API. Here's how it's used on the sign-in form:&lt;/p&gt;

&lt;p&gt;&amp;lt;OAuthConnection &lt;br&gt;
  strategy="oauth_google"    // Required: OAuth provider strategy&lt;br&gt;
  icon="Google"              // Required: Icon name&lt;br&gt;
  lastUsed={lastStrategy}    // Optional: Shows "Last used" badge&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Continue with Google       {/* Required: Button text */}&lt;br&gt;
&lt;br&gt;
The component requires three key props:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;strategy: The OAuth provider identifier (e.g., "oauth_google", "oauth_github") that must exactly match Clerk's strategy names&lt;br&gt;
icon: The icon name to display on the button&lt;br&gt;
children: The button text content&lt;br&gt;
Internally, the component handles the OAuth flow like this:&lt;/p&gt;

&lt;p&gt;async function signInWith(strategy: OAuthStrategy) {&lt;br&gt;
  setLoadingProvider(strategy);&lt;br&gt;
  try {&lt;br&gt;
    return signIn?.authenticateWithRedirect({&lt;br&gt;
      strategy,&lt;br&gt;
      redirectUrl: '/sso-callback',&lt;br&gt;
      redirectUrlComplete: '/',&lt;br&gt;
    });&lt;br&gt;
  } catch (error) {&lt;br&gt;
    return error;&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
async function signUpWith(strategy: OAuthStrategy) {&lt;br&gt;
  setLoadingProvider(strategy);&lt;br&gt;
  try {&lt;br&gt;
    return signUp?.authenticateWithRedirect({&lt;br&gt;
      strategy,&lt;br&gt;
      redirectUrl: '/sso-callback',&lt;br&gt;
      redirectUrlComplete: '/',&lt;br&gt;
    });&lt;br&gt;
  } catch (error) {&lt;br&gt;
    return error;&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
The component calls signIn.authenticateWithRedirect() from the sign-in form and signUp.authenticateWithRedirect() from the sign-up form. The sign-up page passes isSignedUp to select the sign-up OAuth flow explicitly. Both flows redirect through /sso-callback, which is a dedicated page that handles the OAuth handshake completion before sending the user to the home page.&lt;/p&gt;

&lt;p&gt;The component also tracks loading state per provider and displays a "Last used" badge for the most recently used OAuth strategy, improving the UX for returning users.&lt;/p&gt;

&lt;p&gt;Adding a new OAuth provider is a matter of adding a new OAuthConnection instance with the correct strategy prop (matching Clerk's strategy name), an appropriate icon, and enabling the provider in the Clerk dashboard.&lt;/p&gt;

&lt;p&gt;From the user's perspective, OAuth is the fastest path to getting into the app. No password to remember, no verification email to wait for. They click the Google or GitHub button, authorize the app, and they are in. Clerk handles the entire OAuth handshake, token exchange, and session creation behind the scenes.&lt;/p&gt;

&lt;p&gt;Password Reset Flow&lt;br&gt;
The forgot password flow is split across two components: ForgotPasswordForm and ResetPasswordForm.&lt;/p&gt;

&lt;p&gt;The first form collects the user's email and calls:&lt;/p&gt;

&lt;p&gt;await signIn?.create({&lt;br&gt;
  strategy: 'reset_password_email_code',&lt;br&gt;
  identifier,&lt;br&gt;
});&lt;br&gt;
setSuccessfulCreation(true);&lt;br&gt;
This triggers Clerk to send a reset code to the email address. The component then swaps to ResetPasswordForm, which collects the code and the new password together. The reset password schema adds a confirmation field with a cross-field validation:&lt;/p&gt;

&lt;p&gt;export const resetPasswordSchema = z&lt;br&gt;
  .object({&lt;br&gt;
    code: z.string().min(6).max(6),&lt;br&gt;
    password: z.string().min(8).regex(/[a-zA-Z]/).regex(/[0-9]/).regex(/[^a-zA-Z0-9]/),&lt;br&gt;
    confirmPassword: z.string(),&lt;br&gt;
  })&lt;br&gt;
  .refine((data) =&amp;gt; data.password === data.confirmPassword, {&lt;br&gt;
    message: 'Passwords do not match.',&lt;br&gt;
    path: ['confirmPassword'],&lt;br&gt;
  });&lt;br&gt;
The .refine at the end is what enforces that both password fields match before the form can be submitted.&lt;/p&gt;

&lt;p&gt;Middleware-Level Route Protection&lt;br&gt;
All of the above handles the user-facing flows. The middleware is what enforces auth rules at the infrastructure level, before any page code runs.&lt;/p&gt;

&lt;p&gt;Plainform uses clerkMiddleware from @clerk/nextjs/server. It runs on every request and handles two things:&lt;/p&gt;

&lt;p&gt;Redirecting authenticated users away from auth pages&lt;br&gt;
If a signed-in user tries to visit /sign-in, /sign-up, /forgot-password, or /sso-callback, they get redirected to the home page:&lt;/p&gt;

&lt;p&gt;const isProtectedBySignInStatus = createRouteMatcher([&lt;br&gt;
  '/sign-in(.&lt;em&gt;)',&lt;br&gt;
  '/sign-up(.&lt;/em&gt;)',&lt;br&gt;
  '/forgot-password(.&lt;em&gt;)',&lt;br&gt;
  '/sso-callback(.&lt;/em&gt;)',&lt;br&gt;
]);&lt;br&gt;
export default clerkMiddleware(async (auth, req) =&amp;gt; {&lt;br&gt;
  const { userId } = await auth();&lt;br&gt;
  if (userId &amp;amp;&amp;amp; isProtectedBySignInStatus(req)) {&lt;br&gt;
    const url = req.nextUrl.clone();&lt;br&gt;
    url.pathname = '/';&lt;br&gt;
    return NextResponse.redirect(url);&lt;br&gt;
  }&lt;br&gt;
});&lt;br&gt;
This prevents the awkward situation where a logged-in user lands on the sign-in page and sees a form they do not need.&lt;/p&gt;

&lt;p&gt;Validating the order route&lt;br&gt;
The /order route requires a valid Stripe checkout session ID in the query string. The middleware validates this before the page renders:&lt;/p&gt;

&lt;p&gt;if (isOrderRoute(req)) {&lt;br&gt;
  const sessionId = req.nextUrl.searchParams.get('session_id');&lt;br&gt;
  if (!sessionId) {&lt;br&gt;
    return NextResponse.redirect('/');&lt;br&gt;
  }&lt;br&gt;
  try {&lt;br&gt;
    await stripe.checkout.sessions.retrieve(sessionId);&lt;br&gt;
    return NextResponse.next();&lt;br&gt;
  } catch {&lt;br&gt;
    return NextResponse.redirect('/');&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
Anyone trying to access the order confirmation page without a valid session gets bounced back to the home page. This is not auth in the traditional sense, but it is the same pattern: enforce access rules at the edge before any page logic runs.&lt;/p&gt;

&lt;p&gt;Error Handling&lt;br&gt;
Every form in the auth flow uses the same error handling pattern. Clerk returns structured errors with a meta.paramName field that maps directly to form field names. The forms use React Hook Form's setError to attach those errors to the right fields:&lt;/p&gt;

&lt;p&gt;import { isClerkAPIResponseError } from '@clerk/nextjs/errors';&lt;br&gt;
import type { ClerkAPIError } from '@clerk/types';&lt;br&gt;
catch (err: unknown) {&lt;br&gt;
  if (!isClerkAPIResponseError(err)) return;&lt;br&gt;
  err.errors.forEach((error: ClerkAPIError) =&amp;gt; {&lt;br&gt;
    const paramName = error.meta?.paramName as keyof IFormData | undefined;&lt;br&gt;
    if (paramName &amp;amp;&amp;amp; error.longMessage) {&lt;br&gt;
      setError(paramName, {&lt;br&gt;
        type: 'manual',&lt;br&gt;
        message: error.longMessage,&lt;br&gt;
      });&lt;br&gt;
    } else if (paramName === undefined) {&lt;br&gt;
      toast.error(error?.message);&lt;br&gt;
    }&lt;br&gt;
  });&lt;br&gt;
}&lt;br&gt;
Field-level errors show up inline next to the relevant input. Errors that do not map to a specific field — like a network failure or a rate limit — show up as toast notifications.&lt;/p&gt;

&lt;p&gt;User Sync and Database Privacy&lt;br&gt;
Clerk remains the source of truth for identity, but Plainform also keeps a minimal local User record for application data such as Stripe customer IDs and subscription state. The Clerk webhook at /api/webhooks/clerk verifies the webhook signature and then creates, updates, or deletes the local user through Prisma.&lt;/p&gt;

&lt;p&gt;That local User table is private by default. The Supabase RLS migration enables row-level security and does not expose User reads or writes to browser clients using the publishable key. Backend routes and webhooks continue to use Prisma through DATABASE_URL, so Clerk and Stripe sync keep working without exposing user data through the Supabase API.&lt;/p&gt;

&lt;p&gt;How It All Fits Together&lt;br&gt;
The auth system in Plainform is not a single component or a single file. It is a set of pieces that each handle one part of the problem cleanly:&lt;/p&gt;

&lt;p&gt;Zod schemas validate input before it touches the network&lt;br&gt;
Clerk hooks handle the actual identity operations&lt;br&gt;
Custom form components own the UI and UX&lt;br&gt;
The middleware enforces access rules at the edge&lt;br&gt;
Clerk webhooks keep the local Prisma User record in sync&lt;br&gt;
What makes this architecture solid is that each layer has a single responsibility and a clear boundary. The Zod schemas do not know about Clerk. The form components do not know about the middleware. The middleware does not know about the form components. Each piece can be understood, tested, and modified in isolation.&lt;/p&gt;

&lt;p&gt;When you clone Plainform and add your Clerk API keys to the environment variables, all of this works immediately. The sign-up flow, the email verification, the OAuth buttons, the password reset, the middleware protection — it is all there and it is all wired together correctly.&lt;/p&gt;

&lt;p&gt;That is the point. Authentication is not the interesting part of your product. Plainform makes sure it is never the thing slowing you down.&lt;/p&gt;

&lt;p&gt;This article was originally posted on &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;plainform.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>clerk</category>
      <category>development</category>
    </item>
    <item>
      <title>Why We Built Plainform and What It Means for Your Next Project</title>
      <dc:creator>plainform</dc:creator>
      <pubDate>Tue, 28 Apr 2026 00:07:17 +0000</pubDate>
      <link>https://dev.to/plainform/why-we-built-plainform-and-what-it-means-for-your-next-project-54oj</link>
      <guid>https://dev.to/plainform/why-we-built-plainform-and-what-it-means-for-your-next-project-54oj</guid>
      <description>&lt;p&gt;There is a moment every developer knows well. You have an idea. It is a good one. You open your editor, create a new project, and then reality sets in. Before you can build the thing you actually want to build, you have to build a dozen other things first. Authentication. A database. Payments. Email. Storage. Analytics. Each one is its own rabbit hole, and by the time you climb out of all of them, weeks have passed and you have not written a single line of code that is unique to your idea. This is the part nobody talks about when they say they want to develop a web app.&lt;/p&gt;

&lt;p&gt;That is the problem &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;was created to solve. Not by hiding complexity behind a drag-and-drop interface, but by doing the hard setup work once, doing it properly, and handing you the result as a starting point you can trust.&lt;/p&gt;

&lt;p&gt;The Real Cost of Starting From Scratch&lt;br&gt;
There is nothing wrong with building from scratch if you have the time and the goal is to learn. But if you are trying to develop a web app that solves a real problem for real users, time is your most valuable resource. And the hard reality is that most of that time does not go toward the thing that makes your product unique.&lt;/p&gt;

&lt;p&gt;When developers talk about starting a new project, the conversation usually focuses on which framework to use or which database to pick. Those are real decisions, but they are not where most of the time goes. The time goes into integration work. Making Stripe talk to your database. Making sure your authentication middleware does not accidentally expose a route it should protect. Figuring out why your Prisma client is exhausting the connection pool in development.&lt;/p&gt;

&lt;p&gt;Setting up Clerk authentication correctly, including OAuth, custom sign-in flows, and protected routes, takes time. Getting Stripe webhooks to work reliably, with proper signature verification and idempotency handling, takes time. Configuring Prisma with a proper singleton pattern to avoid connection pool issues in development takes time. None of this is hard, but all of it adds up.&lt;/p&gt;

&lt;p&gt;None of this is glamorous work. None of it is the reason you wanted to develop a web app in the first place. And yet it is unavoidable if you want to ship something that works reliably in production.&lt;/p&gt;

&lt;p&gt;The honest truth is that most developers end up solving the same set of problems over and over again, project after project. A new client, a new idea, a new codebase, but the same authentication setup, the same payment integration, the same email configuration. It is repetitive, and repetitive work is exactly the kind of thing that should be automated or templated away. &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;has already done that work. The integrations are not just installed, they are configured correctly and follow best practices from day one.&lt;/p&gt;

&lt;p&gt;What &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;Actually Is&lt;br&gt;
&lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;is a Next.js starter kit built for developers who want to develop a web app without starting from zero every time. It is not a framework on top of a framework. It is not a platform that locks you in. It is a well-structured, production-ready codebase that you clone, configure with your own API keys, and start building on top of.&lt;/p&gt;

&lt;p&gt;The stack is modern and practical. Next.js 16 with the App Router gives you a solid foundation for both server-rendered pages and API routes. TypeScript in strict mode catches mistakes before they become bugs. Tailwind CSS 4 keeps styling fast and consistent. These are not experimental choices. They are the tools that a large portion of the developer community has converged on because they work well together and have strong long-term support.&lt;/p&gt;

&lt;p&gt;What makes &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;different from a blank Next.js project is everything that comes pre-integrated and pre-configured. Authentication, payments, email, storage, a blog, documentation, analytics, and a database layer are all there from the start, wired together correctly and following the patterns that the respective libraries recommend.&lt;/p&gt;

&lt;p&gt;The Integrations That Actually Matter&lt;br&gt;
When you decide to develop a web app for a real audience, there are a handful of things you simply cannot ship without. Users need to be able to sign in. If you are charging for your product, you need payments. You need to be able to reach your users by email. You need somewhere to store files. &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;covers all of this.&lt;/p&gt;

&lt;p&gt;Authentication is handled by Clerk. This is not a minimal username and password setup. It includes OAuth providers, custom sign-in and sign-up flows, password reset, SSO callbacks, and middleware-level route protection. The kind of auth setup that would take a few days to get right from scratch is already there.&lt;/p&gt;

&lt;p&gt;Payments are handled by Stripe. The checkout flow, webhook processing, coupon support, and order management are all included. Stripe webhooks are one of the most common places where developers introduce subtle bugs, usually around signature verification or handling duplicate events. &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt; gets this right from the start.&lt;/p&gt;

&lt;p&gt;Email is handled by Resend, using React Email for templates. This means your email templates are just React components, which makes them easy to build, preview, and maintain. Transactional emails and newsletter subscriptions are both supported.&lt;/p&gt;

&lt;p&gt;The database layer uses PostgreSQL with Prisma ORM. If you have ever dealt with connection pool exhaustion in a Next.js development environment, you know how annoying it is to debug. &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;uses the singleton pattern for the Prisma client, which prevents that problem entirely.&lt;/p&gt;

&lt;p&gt;It Is Not Just About the Tools&lt;br&gt;
A lot of starter kits give you a list of installed packages and call it a day. &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;goes further than that. The integrations are not just present, they are configured the way they should be configured. Environment variables are validated at build time using Zod schemas, so a missing API key fails loudly during the build rather than silently at runtime when a user tries to check out. Routes are protected at the middleware level, not just at the component level. Webhook signatures are verified before any event is processed.&lt;/p&gt;

&lt;p&gt;These are the kinds of details that separate a project that works in development from a project that holds up in production. When you develop a web app and put it in front of real users, the edge cases matter. &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;is built with those edge cases already considered.&lt;/p&gt;

&lt;p&gt;The Content Side of Things&lt;br&gt;
Not every web app is purely functional. Most products benefit from having a blog, a documentation section, or both. These are powerful tools for attracting users, explaining your product, and building trust. But setting up a content system that is fast, searchable, and easy to maintain is its own project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;uses Fumadocs with MDX for both the blog and the documentation section. Writing a new post or a new documentation page is as simple as creating a markdown file in the right folder. The routing, rendering, and search indexing happen automatically. There is no CMS to configure, no API to call, and no build step to trigger manually.&lt;/p&gt;

&lt;p&gt;This post itself is an MDX file sitting in the content/blog folder. That is how straightforward it is.&lt;/p&gt;

&lt;p&gt;Who Should Use &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt;&lt;br&gt;
&lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;is a good fit for a few different kinds of developers. If you are an indie developer or a small team trying to develop a web app and get it in front of users as quickly as possible, &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;removes the weeks of setup work that would otherwise stand between your idea and your first user.&lt;/p&gt;

&lt;p&gt;If you are a freelancer who builds web applications for clients, &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;gives you a reliable, well-structured starting point that you can reuse across projects. Every time a client asks you to develop a web app for them, you are not starting from a blank slate. The code is clean, the patterns are consistent, and the integrations are solid enough that you can hand the project off to a client without worrying about what you left behind.&lt;/p&gt;

&lt;p&gt;If you are a developer who has built these integrations before and is tired of doing it again, &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform &lt;/a&gt;is simply a time saver. You already know what good looks like. &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt; gives you that without the repetition.&lt;/p&gt;

&lt;p&gt;What &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt; Is Not&lt;br&gt;
It is worth being clear about what &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt; is not, because the wrong expectations will lead to frustration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt; is not a no-code tool. You will write code. You will need to understand the technologies involved. If you are not comfortable with Next.js, TypeScript, and the basics of the integrations included, there will be a learning curve.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt; is not a managed platform. There is no dashboard, no hosted version, no support contract. You run it, you own it, you maintain it. That is a trade-off, but for most developers it is the right one. Owning your code means you can change anything, optimize anything, and are not dependent on a third-party platform staying in business.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt; is also not a finished product. It is a starting point. The value is in what you build on top of it.&lt;/p&gt;

&lt;p&gt;A Different Way to Think About Starting&lt;br&gt;
Most developers think of a starter kit as a shortcut. Something that gets you to "hello world" faster. &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt; is more than that. It is an attempt to define what a well-built foundation looks like for a modern web application, and then give that foundation to anyone who wants to develop a web app without reinventing it.&lt;/p&gt;

&lt;p&gt;The goal is not to save you an afternoon. The goal is to save you the first two or three weeks of a project, and to make sure that the foundation you are building on is solid enough that it does not cause problems six months down the road.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;br&gt;
If you have ever started a new project and felt the weight of all the setup work ahead of you before you could get to the interesting part, &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;Plainform&lt;/a&gt; was built for you. It is a practical, production-ready way to develop a web app without the repetitive groundwork that slows most projects down at the start.&lt;/p&gt;

&lt;p&gt;The stack is modern. The integrations are solid. The code is yours. Give it a try and see how much faster you can move when the foundation is already in place.&lt;/p&gt;

&lt;p&gt;This article first appeared on &lt;a href="https://plainform.dev/" rel="noopener noreferrer"&gt;plainform.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>softwaredevelopment</category>
      <category>tooling</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
