I've been wanting to build something with AI for a while now. Not just a wrapper around ChatGPT, but something that actually feels useful. So I built CrafticWeb — you describe a website, and it generates real HTML/CSS/JS for you. You can tweak it with follow-up prompts, edit the code directly, preview it live, and deploy it with one click.
It's live, it works, and I learned a ton building it. Here's the honest story.
The Idea
The core concept is simple. You type something like "a landing page for a coffee shop with a dark theme" and the app sends that to an AI model (DeepSeek via OpenRouter), gets back a full HTML document, and renders it in the browser instantly. You can keep chatting to refine it, open it in a Monaco editor to manually tweak things, and when you're happy — deploy it to a public URL.
I also added Google login, a credits system, and Stripe billing because I wanted to build something that felt like a real product, not just a weekend demo.
The Stack
For the frontend I went with React + Vite, Redux for state, Tailwind for styling, Framer Motion for animations, and Monaco Editor for the in-browser code editor. The backend is Node.js + Express, MongoDB for storage, JWT with httpOnly cookies for auth, and OpenRouter to talk to the AI. Stripe handles billing.
Nothing too exotic. The interesting stuff is in how it all fits together.
What Actually Broke (And How I Fixed It)
Getting the AI to return clean JSON
This was my first real headache. The backend sends a master prompt to DeepSeek via OpenRouter and expects back a clean JSON object like this:
{
"message": "Your website is ready",
"code": "<full HTML document>"
}
Simple enough in theory. In practice, the model kept wrapping its response in markdown code fences — you know, the triple backtick blocks — which meant JSON.parse() would just throw and the whole thing would fail. My fix was a small utility function that strips out those fences before parsing, and if the JSON is still broken, the backend retries the API call up to three times. Once I had that in place, generation became reliable.
Rendering the preview without CORS exploding
My first attempt at the live preview used srcdoc to inject HTML into an iframe. Vite did not like that. It kept injecting its own scripts into the content and everything would break with CORS errors.
The fix was switching to Blob URLs. Instead of passing the HTML as a string attribute, you create a Blob from it, generate a local object URL, and point the iframe's src at that:
const blob = new Blob([html], { type: "text/html" });
const url = URL.createObjectURL(blob);
iframeRef.current.src = url;
The browser treats it like a local file, completely sandboxed, no CORS, no Vite interference. One small change, completely solved it.
Authentication Flow
Google Auth looks simple from the outside but there are a few moving pieces. Here's the full flow:
- User clicks "Continue with Google"
- Firebase
signInWithPopupreturns the user's info - Frontend POSTs
{ name, email, avatar }to/api/v1/auth/register - Backend creates or finds the user in MongoDB, generates a JWT, and sets it as an
httpOnlycookie - All subsequent requests send the cookie automatically via
withCredentials: true
On the frontend every Axios call goes out with credentials attached:
axios.defaults.withCredentials = true;
And on the backend the cookie is set like this:
res.cookie("token", jwt, {
httpOnly: true,
secure: true,
sameSite: "none",
});
The httpOnly flag means JavaScript on the client can never read the token directly — much safer than storing it in localStorage. And sameSite: 'none' + secure: true is what makes the cookie actually travel cross-origin between your deployed frontend and backend.
Cookies not working after deployment
This one burned me for an embarrassingly long time. I deployed to Render, everything looked fine locally, and then authenticated requests just stopped working in production. The cookie was being set, but the browser wasn't sending it back with requests.
Turned out I needed two things together: sameSite: 'none' and secure: true on the cookie, and NODE_ENV=production set in Render's environment variables. Without that last one, the backend was still behaving like it was in development mode and the production cookie settings never kicked in. Once both were set, it worked perfectly.
Google OAuth popup silently failing
Firebase's signInWithPopup was failing with auth/popup-closed-by-user even when the user hadn't closed anything. After digging around I found the culprit — a Cross-Origin-Opener-Policy header I'd added to the backend. It was preventing the OAuth popup from communicating back to the parent window. Removing that header fixed it completely.
The Thing I'm Most Proud Of
Honestly? The credits and billing system. It felt intimidating before I built it, but it's pretty clean now. Stripe Checkout handles the payment UI, Stripe Webhooks confirm the payment server-side and add credits to the user's account in MongoDB, and credits get deducted on each successful generation. The one thing I'd warn anyone about — use express.raw() as the body parser for your webhook route, not express.json(). The second one breaks Stripe's signature verification and you'll spend an hour wondering why webhooks aren't firing.
What's Next
I want to add streaming so the HTML appears progressively instead of all at once after a delay — that would make it feel way snappier. Version history is on the list too, so you can go back to an earlier generation if you over-prompted yourself into a mess.
If you want to check it out or dig into the code, it's all open source.
Built with too much console.log and not enough sleep — Kunal




















