Skip to content
Armin's Portfolio
Go back

artefficient.io architecture and engineering trade-offs

artefficient.io is built as a two-platform product:

I made that split on purpose. I wanted the main app focused on the product experience, while the CMS handled editorial work as its own system. That gave me cleaner boundaries and made it easier to evolve each side without constantly stepping on the other.

This post is a straight engineering write-up of the stack, the reasoning behind it, and the trade-offs that came with it.

Table of contents

Open Table of contents

Product architecture at a glance

At a high level, the architecture has two production runtimes and a clear boundary between them:

  1. Experience runtime (Next.js on Vercel):

    • localized marketing/product pages
    • server route handlers under app/api/*
    • lead/contact/careers submission endpoints
    • AI chat endpoint for the Tuti assistant
  2. Content runtime (Strapi on Cloud Run):

    • blog content model with SEO fields and localization
    • job opening model with hiring-specific fields
    • editor/admin workflows in Strapi admin
    • media upload management via Google Cloud Storage
  3. Integration contract between both runtimes:

    • Next.js fetches CMS content using token-authenticated server calls
    • selected Next API routes proxy/filter Strapi data for frontend consumption
    • locale alignment is maintained between frontend routes and CMS content locales

The key decision here was separation of concerns:

It does increase the operational surface area, but I found that trade worth it because ownership and scaling boundaries became much clearer.

Frontend platform and runtime choices

Why I used Next.js App Router + TypeScript

The root app uses modern Next.js with React + TypeScript.

It was a good fit for a product site that needed:

Trade-offs:

Why I used next-intl for localization

Localization is first-class in the app, with middleware + locale-aware routing (en, fr).

This gave me:

Trade-offs:

Design system and motion stack

Tailwind-first UI with composable primitives

The UI stack combines:

For this project, it was the fastest way to move without rebuilding primitives from scratch.

Why it works here:

Trade-offs:

Motion and storytelling

The product uses Framer Motion and GSAP to create a visually expressive, conversion-focused experience.

I used motion for two main reasons:

Trade-offs:

AI assistant integration approach

Vercel AI SDK + OpenAI through server route handlers

The chat endpoint (app/api/chat/route.ts) uses the AI SDK streaming model with OpenAI, and the frontend chat interface consumes streamed responses.

This worked well for:

Trade-offs:

Prompt strategy and brand voice

The assistant prompt is intentionally constrained to:

That was a deliberate product choice. I wanted the assistant to feel like a guided conversion touchpoint, not a general-purpose chatbot.

Trade-offs:

Content architecture with Strapi

Why I kept Strapi v5 as a separate CMS service

The CMS (cms/strapi-project) is a full Strapi v5 TypeScript app with:

This gave non-engineering users direct publishing control while keeping the frontend focused on presentation and conversion.

Blog model design: SEO as a first-class concern

The blog schema includes:

I liked this model because SEO metadata lived with each entry instead of being hardcoded in frontend templates.

Trade-offs:

Job opening model design: operational hiring workflows

The job model includes:

It also meant job listings could be updated without touching the frontend codebase.

Trade-offs:

Media strategy: Google Cloud Storage upload provider

Strapi is configured with the GCS upload provider and CSP rules that explicitly allow the bucket domain.

Why I liked this choice:

Trade-offs:

API and data access patterns

BFF pattern in Next route handlers

Next route handlers are used as a backend-for-frontend (BFF) layer for:

Using route handlers as a BFF kept provider integrations on the server and kept secrets out of the browser.

CMS fetch strategy and cache behavior

lib/blog-service.ts uses tagged fetches and revalidation windows for content retrieval, including sitemap and SEO-oriented endpoints.

That improved read performance and let me control content freshness without rebuilding for every change.

Trade-offs:

Rewrites and runtime decoupling

next.config.js rewrites /strapi-admin/* and /strapi-api/* to the CMS host.

This made local and production URLs easier to manage and let the frontend and CMS feel cohesive from the outside while still being independently deployable.

Trade-offs:

Security, trust, and abuse controls

What is already in place

Important current gaps and risks

These are all normal growth-stage issues, but they are the parts I would tighten first as traffic and team size grow.

Deployment and operations model

Frontend deployment: Vercel

The app is configured for Vercel with region targeting and standalone output.

Operationally, this yields:

CMS deployment: Cloud Run + Cloud SQL + Secret Manager

The Strapi service uses:

For this kind of project, I found this to be a practical production setup.

Trade-offs:

Environment variable operations

The project includes scripts to sync .env variables into Vercel environments.

That helped keep deployments more consistent and reduced manual dashboard drift.

Trade-off: script-based secret handling must be audited carefully to avoid accidental leakage in local shell history/logs.

Developer experience and quality posture

Tooling choices

DX is built around:

Nothing fancy here, just a practical baseline that keeps the project maintainable.

Testing and verification posture

The repository shows strong product implementation velocity, but limited visible automated test coverage and no obvious CI pipeline in the repository itself.

Combined with build-time type/lint bypass settings, this creates a classic trade-off:

That trade-off made sense earlier in the project, but it is one of the first things I would tighten as the product expands.

Major trade-offs and what I would tighten next

This architecture reflects a pretty typical startup-style set of trade-offs, and I was fine making them at the time.

I chose separation over simplicity by keeping the app and CMS independent. I chose velocity over strict quality gates in a few places so I could keep shipping. I chose a richer visual and motion-heavy experience even though that raised the bar for performance discipline. And I leaned on managed platforms like Vercel and GCP because they removed a lot of platform work, even if that meant more vendor coupling.

If I were hardening the project further, I would start by re-enabling strict type and lint failures in CI, adding rate limiting and bot protection to every public write endpoint, normalizing the Strapi environment variable strategy, adding baseline integration tests for the main conversion and content flows, and wiring up unified monitoring across the frontend and CMS.

Closing thoughts

What I like about artefficient.io is that it feels like a real product system, not just a polished frontend sitting on top of a few mock APIs. The architecture reflects actual concerns: multilingual content, editorial control, lead capture, AI-assisted interactions, media handling, and deployment across more than one platform.

It is not perfect, and I would not pretend otherwise. But the trade-offs feel honest. The stack was chosen to support the product we were trying to build, and most of the rough edges are exactly where you would expect them to be in a fast-moving product that is still maturing.


Share this post on:

Previous Post
My homelab architecture and why I built it this way
Next Post
Polaris engineering write-up