Fullstack-Agentur finden: Worauf es wirklich ankommt
Das Problem mit dem Begriff "Fullstack"
Jede zweite Agentur nennt sich heute "Fullstack". Das Problem: der Begriff ist nicht geschützt und wird inflationär verwendet. Eine WordPress-Agentur, die ein paar REST-Calls über ein Plugin macht, bezeichnet sich genauso als Fullstack wie ein Engineering-Studio, das eigene Deployment-Pipelines, Infrastructure-as-Code und typisierte APIs betreibt.
Die Konsequenz: du vergleichst Äpfel mit Birnen. Preise variieren um Faktor 5, weil die Leistungen hinter dem gleichen Label völlig unterschiedlich sind.
Wir haben als Engineering-Team selbst schon Code von Agenturen übernommen, der "professionell" entwickelt wurde. Manche Repositories waren sauber und durchdacht. Andere waren ein einziger any-Friedhof. Dieser Artikel zeigt dir, wie du den Unterschied erkennst — bevor du unterschreibst.
Code lügt nicht: Good vs. Bad Engineering
Bevor wir über Soft-Kriterien reden, schauen wir uns an, was den Unterschied auf Code-Ebene ausmacht. Hier ist dasselbe Feature — ein API-Endpoint zur Nutzerregistrierung — einmal schlecht und einmal professionell implementiert.
So sieht schlechter Code aus
// POST /api/register — die "Hauptsache es funktioniert"-Version
app.post('/api/register', async (req, res) => {
const { email, password, name } = req.body
// Keine Validierung. Überhaupt keine.
// SQL Injection? Klar, komm rein.
const result = await db.query(
`INSERT INTO users (email, password, name)
VALUES ('${email}', '${password}', '${name}')
RETURNING *`
)
// Passwort im Klartext gespeichert. Kein Hashing.
// Und wir senden es auch gleich in der Response mit zurück.
console.log('User registriert:', result.rows[0])
res.json({ success: true, user: result.rows[0] })
})
Was hier alles falsch läuft:
- SQL Injection durch String-Interpolation — ein Angreifer kann die gesamte Datenbank löschen
- Klartext-Passwörter — DSGVO-Verstoß und Sicherheitsrisiko
- Keine Input-Validierung — leere Felder, ungültige E-Mails, alles geht durch
console.login Production — persönliche Daten im Server-Log- Kein Error-Handling — bei einem Datenbankfehler crasht der gesamte Prozess
- Kein Rückgabetyp — niemand weiß, was die API zurückgibt
So sieht professioneller Code aus
// schemas/auth.ts — Validierung mit Zod
import { z } from 'zod'
export const registerSchema = z.object({
email: z.string().email('Ungültige E-Mail-Adresse'),
password: z
.string()
.min(8, 'Passwort muss mindestens 8 Zeichen haben')
.regex(/[A-Z]/, 'Mindestens ein Großbuchstabe erforderlich')
.regex(/[0-9]/, 'Mindestens eine Zahl erforderlich'),
name: z.string().min(2).max(100),
})
export type RegisterInput = z.infer<typeof registerSchema>
// routes/auth.ts — der gleiche Endpoint, richtig gemacht
import { Router } from 'express'
import bcrypt from 'bcrypt'
import { registerSchema } from '../schemas/auth'
import { db } from '../lib/database'
import { AppError } from '../lib/errors'
import { logger } from '../lib/logger'
const router = Router()
router.post('/register', async (req, res, next) => {
try {
// 1. Input-Validierung
const parsed = registerSchema.safeParse(req.body)
if (!parsed.success) {
throw new AppError(400, 'Ungültige Eingabedaten', parsed.error.flatten())
}
const { email, password, name } = parsed.data
// 2. Prüfen ob E-Mail bereits existiert
const existing = await db.query(
'SELECT id FROM users WHERE email = $1',
[email]
)
if (existing.rows.length > 0) {
throw new AppError(409, 'Diese E-Mail-Adresse ist bereits registriert')
}
// 3. Passwort hashen — nie im Klartext speichern
const passwordHash = await bcrypt.hash(password, 12)
// 4. Parameterisierte Query — SQL Injection unmöglich
const result = await db.query(
`INSERT INTO users (email, password_hash, name)
VALUES ($1, $2, $3)
RETURNING id, email, name, created_at`,
[email, passwordHash, name]
)
// 5. Strukturiertes Logging — ohne sensible Daten
logger.info('User registered', { userId: result.rows[0].id })
// 6. Kein Passwort-Hash in der Response
res.status(201).json({
success: true,
user: result.rows[0],
})
} catch (error) {
next(error) // zentrales Error-Handling
}
})
Der Unterschied ist nicht "besser" vs. "schlechter". Der erste Code ist ein Sicherheitsvorfall, der darauf wartet, passiert zu werden. Der zweite ist Standard-Engineering. Wenn eine Agentur dir Code liefert, der aussieht wie Beispiel 1, hast du ein ernstes Problem.
Wer arbeitet tatsächlich an deinem Projekt?
Die wichtigste Frage — und die, die am häufigsten falsch beantwortet wird. Viele Agenturen verkaufen mit Senior-Profilen im Pitch und staffieren dann mit Junioren oder Offshore-Freelancern. Das merkst du oft erst nach Wochen, wenn die Code-Qualität nicht zum Pitch passt.
Frage konkret:
- Welche Personen werden an meinem Projekt arbeiten? Vor- und Nachnamen, LinkedIn.
- Haben sie nachweisbare Erfahrung mit dem konkreten Tech-Stack?
- Werden dieselben Personen durchgehend am Projekt arbeiten?
- Kann ich einen Code-Review eines bestehenden Projekts sehen?
Ein Team-Wechsel mitten im Projekt kostet dich mindestens 2-3 Wochen Onboarding — Zeit, die du bezahlst.
CI/CD: Der schnellste Qualitätsindikator
Frage jede Agentur nach ihrer Deployment-Pipeline. Wenn die Antwort "wir deployen manuell per FTP" oder ein langes Schweigen ist, hast du deine Antwort.
Eine professionelle Fullstack-Agentur hat eine automatisierte Pipeline, die bei jedem Push Tests ausführt, den Build validiert und bei Erfolg automatisch deployed. Das ist kein Nice-to-Have — das ist Grundvoraussetzung.
So sieht eine reale CI/CD-Pipeline aus:
# .github/workflows/deploy.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Type Check
run: pnpm tsc --noEmit
- name: Lint
run: pnpm eslint . --max-warnings 0
- name: Unit Tests
run: pnpm vitest run --coverage
- name: Upload Coverage
uses: codecov/codecov-action@v4
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: quality
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: ${{ secrets.TEST_DB_PASSWORD }}
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm db:migrate
env:
DATABASE_URL: postgresql://test_user:${{ secrets.TEST_DB_PASSWORD }}@localhost:5432/test_db
- name: Playwright Tests
run: pnpm playwright test
env:
DATABASE_URL: postgresql://test_user:${{ secrets.TEST_DB_PASSWORD }}@localhost:5432/test_db
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [quality, e2e]
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy
run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
Was du hier siehst: Type-Checking, Linting, Unit-Tests, E2E-Tests mit einer echten Datenbank und automatisches Deployment — erst wenn alles grün ist. Kein manueller Eingriff nötig. Kein "ich deploy das mal schnell vom Laptop".
Frage die Agentur: "Zeig mir eure Pipeline." Wenn es keine gibt, ist das ein Ausschlusskriterium.
Development-Setup: Docker Compose als Mindeststandard
Ein weiterer schneller Test: Wie sieht das lokale Development-Setup aus? Ein professionelles Projekt braucht genau einen Befehl, um die gesamte Entwicklungsumgebung zu starten. Nicht drei README-Seiten mit manuellen Installationsschritten.
# docker-compose.yml — komplette Entwicklungsumgebung
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- '3000:3000'
volumes:
- .:/app
- /app/node_modules
environment:
- DATABASE_URL=postgresql://dev:dev@postgres:5432/app_dev
- REDIS_URL=redis://redis:6379
- NODE_ENV=development
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
postgres:
image: postgres:16-alpine
ports:
- '5432:5432'
environment:
POSTGRES_DB: app_dev
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U dev']
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
postgres_data:
Ein docker compose up — und alles läuft. Datenbank, Cache, App. Jeder neue Entwickler ist in 5 Minuten arbeitsfähig, nicht in 5 Stunden.
Red Flags im Code: Was schlechte Agenturen liefern
Wir haben in den letzten Jahren Code von verschiedenen Agenturen übernommen und auditiert. Hier sind die Muster, die immer wieder auftauchen — und die sofort zeigen, ob jemand Engineering versteht oder nur "es funktioniert" abliefert.
Red Flag 1: any als Standard-Typ
// Wenn du das in einem TypeScript-Projekt siehst, war es kein TypeScript-Projekt.
// Es war ein JavaScript-Projekt mit .ts-Dateiendungen.
const fetchData = async (url: any): Promise<any> => {
const response: any = await fetch(url)
const data: any = await response.json()
return data
}
// Oder noch besser: tsconfig mit "strict: false"
// Dann kann man sich TypeScript auch gleich sparen.
TypeScript mit any überall ist schlimmer als JavaScript — es gibt dir ein falsches Sicherheitsgefühl. Du denkst, du hast Typsicherheit, aber in Wirklichkeit hast du gar nichts.
Red Flag 2: Secrets im Code
// Das hier sollte NIEMALS im Repository landen.
// Und nein, .env ins .gitignore zu packen reicht nicht,
// wenn die Werte direkt im Code stehen.
const stripe = new Stripe('sk_live_51ABC123...')
const dbUrl = 'postgresql://admin:SuperSecret123@prod-db.example.com:5432/production'
// Richtig: Environment Variables
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const dbUrl = process.env.DATABASE_URL!
Wir haben schon Projekte übernommen, in denen Production-Datenbankpasswörter im Git-Verlauf standen. Sobald ein Secret einmal committed wurde, ist es kompromittiert — selbst wenn du es danach löschst. Die Git-History vergisst nicht.
Red Flag 3: Keine Tests, nur console.log
// "Debugging" in Production — ein Klassiker
export async function processPayment(order: any) {
console.log('order:', order)
console.log('processing...')
const result = await stripe.charges.create({
amount: order.total, // Ist das in Cent? Euro? Wer weiß.
currency: 'eur',
source: order.token,
})
console.log('result:', result)
console.log('DONE!!!!!')
return result
}
Wenn console.log das einzige Debugging-Tool ist und es null Tests gibt, wurde dieser Code nie ernsthaft reviewt. In einer professionellen Codebase gibt es strukturiertes Logging (Winston, Pino), Error-Tracking (Sentry) und Tests, die vor jedem Deployment laufen.
Evaluation Scorecard: Agenturen systematisch bewerten
Nutze diese Scorecard bei deinen Discovery Calls. Bewerte jede Agentur auf einer Skala von 1-5 und vergleiche die Ergebnisse objektiv.
| Kriterium | 1 (Schlecht) | 3 (Akzeptabel) | 5 (Exzellent) | |---|---|---|---| | Team-Kontinuität | "Wir suchen noch die richtigen Leute" | Festes Team, aber Wechsel möglich | Festes, namentlich benanntes Team mit Track Record | | Tech-Stack-Tiefe | 47 Logos auf der Website, alles ein bisschen | Klarer Stack, aber wenig Architektur-Expertise | Klarer Stack mit begründeten Architektur-Entscheidungen | | CI/CD-Reife | Manuelles Deployment, kein Staging | Automatisierter Build, aber Tests fehlen | Vollständige Pipeline: Lint, Types, Tests, Preview, Production | | Code-Ownership | "Ihr bekommt den Code nach Projektende" | Shared Repository, Zugang auf Anfrage | Dein Repository von Tag 1, du bist Owner | | Kommunikation | Monatliche Status-Updates per E-Mail | Wöchentliche Calls mit Zusammenfassung | Async-Updates via Slack/GitHub, täglicher Commit-Zugang | | NDA / IP-Schutz | NDA nur auf Nachfrage, unklare IP-Regelung | Standard-NDA vorhanden | NDA vor dem ersten Gespräch, IP liegt bei dir, vertraglich fixiert | | Delivery Track Record | Keine Referenzen oder nur vage Beschreibungen | 1-2 vergleichbare Projekte vorweisbar | Mehrere vergleichbare Projekte mit messbaren Ergebnissen |
Minimum für eine seriöse Zusammenarbeit: Durchschnitt von 3 oder höher, kein Einzelwert unter 2. Wenn CI/CD oder Code-Ownership unter 3 liegt, ist das ein ernstes Warnsignal.
Tech-Stack Interview: 7 Fragen, die du stellen solltest
Diese Fragen trennen Agenturen, die Engineering verstehen, von solchen, die nur implementieren.
1. "Wie stellt ihr Type Safety über den gesamten Stack sicher?"
- Schlechte Antwort: "Wir nutzen TypeScript." (Punkt. Keine Details.)
- Gute Antwort: "Wir nutzen TypeScript strict mode, generieren API-Types aus dem Schema (z.B. mit Zod oder tRPC), und unsere Datenbank-Queries sind ebenfalls typisiert — zum Beispiel mit Drizzle oder Prisma. End-to-End Type Safety vom Frontend bis zur Datenbank."
2. "Wie handhabt ihr Datenbank-Migrationen in Production?"
- Schlechte Antwort: "Wir passen das Schema manuell an." oder "Wir exportieren und importieren."
- Gute Antwort: "Versionierte Migrations-Dateien, die automatisch im Deployment laufen. Jede Migration ist reversibel. Wir testen Migrationen erst gegen eine Staging-Datenbank, bevor sie in Production laufen."
3. "Was passiert, wenn nachts um 3 Uhr euer Deployment fehlschlägt?"
- Schlechte Antwort: "Das ist uns noch nie passiert."
- Gute Antwort: "Automatischer Rollback auf die letzte funktionierende Version. Alerting über PagerDuty/Opsgenie. Health-Checks, die fehlgeschlagene Deployments erkennen, bevor Traffic geroutet wird."
4. "Wie geht ihr mit Environment-spezifischer Konfiguration um?"
- Schlechte Antwort: "Wir haben eine config.js mit if-else für Development und Production."
- Gute Antwort: "Environment Variables, validiert beim App-Start mit einem Schema. Secrets liegen in einem Secret Manager (z.B. Vault, AWS Secrets Manager) oder als verschlüsselte GitHub Secrets. Nichts davon ist im Repository."
5. "Zeigt mir euren Test-Ansatz für ein typisches Projekt."
- Schlechte Antwort: "Wir testen manuell." oder "Wir schreiben Tests, wenn am Ende noch Zeit ist."
- Gute Antwort: "Unit-Tests für Business-Logik, Integration-Tests für API-Endpoints mit einer echten Test-Datenbank, E2E-Tests für kritische User-Flows. Tests laufen automatisch in der CI-Pipeline und blocken das Deployment bei Failures."
6. "Wie strukturiert ihr ein Monorepo mit Frontend und Backend?"
- Schlechte Antwort: "Frontend und Backend sind in separaten Repos ohne geteilte Types."
- Gute Antwort: "Monorepo mit Turborepo oder Nx. Geteilte Packages für Types, Validierung und Utilities. Abhängigkeiten sind klar definiert, Builds laufen parallel und gecacht."
7. "Was ist eure Strategie für Error-Handling und Observability?"
- Schlechte Antwort: "Wir loggen Fehler in die Console und schauen regelmäßig rein."
- Gute Antwort: "Sentry für Error-Tracking mit Source-Maps, strukturiertes Logging mit Correlation-IDs, Uptime-Monitoring. Bei kritischen Fehlern werden wir automatisch benachrichtigt, nicht erst wenn der Kunde sich meldet."
Wenn eine Agentur auf mehr als zwei dieser Fragen keine überzeugende Antwort hat, fehlt die technische Tiefe, die du für ein ernstes Produkt brauchst.
Vertraulichkeit und IP: Nicht verhandelbar
Besonders für Startups mit innovativen Ideen ist das Thema IP kritisch:
- NDA vor dem ersten Gespräch — nicht erst auf Anfrage. Jede seriöse Agentur hat ein Standard-NDA parat.
- Klare IP-Vereinbarung — der gesamte Code gehört dir. Keine Klauseln, die der Agentur Rechte an "wiederverwendbaren Komponenten" einräumen.
- Kein Showcase deines Projekts ohne deine explizite, schriftliche Genehmigung.
- Repository-Zugang von Tag 1. Du bist Owner, nicht Gast.
Der richtige Zeitpunkt
Du brauchst eine Fullstack-Agentur, wenn du ein technisches Produkt bauen willst, aber kein eigenes Engineering-Team hast — oder wenn dein Team Verstärkung braucht, die vom ersten Tag an auf Senior-Level liefert.
Nimm dir die Zeit für 2-3 Discovery Calls. Nutze die Scorecard. Stelle die Interview-Fragen. Und lass dir Code zeigen — nicht Slide Decks.
Wie wir bei SecretStack arbeiten
Wir sind ein KI-natives Fullstack Engineering Studio. Unser Stack: Next.js, TypeScript, Tailwind, Node.js/Python, PostgreSQL. Nicht weil es trendy ist, sondern weil dieser Stack für 90% der SaaS- und KI-Produkte optimal funktioniert.
Was das in der Praxis bedeutet:
- Festes Team — die Leute, die im Pitch sitzen, arbeiten auch an deinem Projekt
- Dein Repository, dein Code — von Tag 1
- CI/CD ab Sprint 1 — nicht als Nachgedanke in Woche 8
- Async-Kommunikation — tägliche Commits, wöchentliche Demos, Slack für alles dazwischen
- NDA standardmäßig — bevor wir überhaupt über dein Projekt sprechen
Klingt nach dem, was du suchst? Buch einen Discovery Call — wir schauen uns gemeinsam an, ob und wie wir dein Projekt umsetzen können.