Back to projects
CASE 01 · VISIBLE BACKENDWorking product MVP · evolving

TATTOTOYOU — an AI assistant for a tattoo studio

The service handles initial Telegram enquiries, answers from verified studio knowledge, gathers application details, and hands medical, sensitive, or unusual conversations to the artist.

Role
Backend, architecture, and AI integration
System
Telegram · API · Queue · AI · CRM
Delivery model
Durable Inbox/Outbox · at-least-once
PROBLEM & CONSTRAINTS

Automate repetitive work without taking control from the artist.

Tattoo clients often ask the same questions about pricing, styles, preparation, and aftercare. Other conversations involve health, conflict, personal assessment, or an actual booking — decisions that should not be delegated blindly to an LLM.

01 / PROBLEM

What the system needs to do

Accept an enquiry quickly, identify the scenario, respond from verified knowledge, preserve context, and turn fragmented chat messages into a structured application for the artist.

02 / CONSTRAINTS

Important constraints

  • Telegram may deliver the same update more than once.
  • Redis, Telegram, and the AI API can fail temporarily.
  • Medical and sensitive conversations require a person.
  • Phone numbers and email addresses should not be sent to an external AI unnecessarily.
  • The assistant must not invent pricing, policies, or booking details.
ARCHITECTURE

Persist the work first, then perform external side effects.

The webhook does not pass an update directly into business logic. FastAPI validates and atomically stores it in PostgreSQL Inbox. The queue receives only the persisted event ID, while each outgoing reply is delivered through a separate Transactional Outbox.

TATTOTOYOU architecture: Telegram, FastAPI, PostgreSQL Inbox, Redis and Celery, worker, RAG and GigaChat, PostgreSQL Outbox, and Telegram delivery.
  1. 1. Telegram: webhook
  2. 2. FastAPI: validate
  3. 3. PG Inbox: durable
  4. 4. Redis / Celery: schedule
  5. 5. Worker: process
  6. 6. RAG / GigaChat: answer
  7. 7. PG Outbox: persist / retry
  8. 8. Telegram Bot API: send / retry
Celery Beat recoveryCelery Beat recovery

The scheduler re-enqueues ready or stuck Inbox and Outbox records, so a broker failure after the database commit does not erase accepted work.

Inbox: re-enqueueOutbox: retry delivery
  • Idempotent inbound processing
  • Single business response in PostgreSQL
  • At-least-once outbound delivery
EVENT FLOW

An event moves through inspectable states, not a hidden chain of functions.

Each stage exposes its input, action, and result. Scenario controls show normal delivery, broker recovery, and a conversation handoff to the artist.

Execution scenario
  1. TELEGRAM

    Incoming message

    Telegram delivers an update and may send the same event again when network acknowledgement is uncertain.

    Input
    update_id, chat_id, message text
    Action
    HTTPS webhook delivery
    Result
    Request to the public endpoint
  2. FASTAPI WEBHOOK

    System boundary checks

    The API limits request size, verifies the Secret Token, and applies rate limiting before business processing begins.

    Input
    HTTP headers and Telegram payload
    Action
    Validate, authenticate, normalise
    Result
    Verified update
  3. POSTGRESQL INBOX

    Durable acceptance

    The update is stored before Redis is contacted. A unique external ID turns repeated delivery into an idempotent lookup.

    Input
    Verified Telegram update
    Action
    INSERT or resolve existing event
    Result
    Inbox UUID in ready state
  4. CELERY WORKER

    Claim and classification

    The worker receives the UUID, locks the record, loads the conversation, and evaluates intent, safety signals, and ownership.

    Input
    Inbox UUID and persisted context
    Action
    Claim, classify, apply state rules
    Result
    Decision: AI, fallback, or handoff
  5. RAG / GIGACHAT

    Answer from verified knowledge

    Retrieval finds relevant knowledge, PII is masked, and generation proceeds only when the available context is strong enough.

    Input
    Intent and sanitised context
    Action
    Retrieve, score, generate or abstain
    Result
    Grounded reply or escalation signal
  6. POSTGRESQL OUTBOX

    Persist and deliver the result

    The business reply and delivery record commit together. Temporary Telegram failures enter an independent retry cycle.

    Input
    Reply, fallback, or handoff command
    Action
    Commit result, enqueue delivery
    Result
    Delivered message or retry state
  7. ARTIST / CRM

    Human control

    After escalation, AI is blocked while the artist sees the conversation, lead data, and context, then replies or returns ownership.

    Input
    Escalated or human_owned conversation
    Action
    Review, reply, update lead and booking
    Result
    Artist decision with preserved history
KEY DECISIONS

Reliability is designed around boundaries with the outside world.

The core decisions respond to concrete risks: lost work, duplicate processing, unreliable delivery, and AI answers without sufficient evidence.

DURABILITY

PostgreSQL Inbox before Celery

The accepted update is committed first; queueing its UUID is best effort. Recovery covers the broker-unavailable window.

DELIVERY

Transactional Outbox

The reply and its delivery job commit together. Telegram delivery is separated from the business result and has its own retry state.

CONTROL

Handoff as a state machine

Once escalated, AI stops replying while new client messages remain stored. Conversation ownership becomes explicit state.

AI SAFETY

RAG without mandatory generation

If verified knowledge is insufficient, the system does not ask the LLM to fill the gap. It creates a safe fallback and transfers the conversation.

PRIVACY

PII minimisation

Phone numbers and email addresses are masked before external AI calls, TLS verification remains enabled, and original text stays in PostgreSQL.

CONCURRENCY

Locks and unique invariants

FOR UPDATE SKIP LOCKED and partial unique indexes protect event processing, the single business reply, and one open conversation per client.

FAILURE SCENARIOS

What happens when a dependency leaves the happy path.

The system distinguishes temporary from permanent failures, keeps technical detail in logs, and produces a safe user-facing outcome.

FailureSystem responseOutcome
Redis is unavailable after webhook persistence

Inbox remains ready; Celery Beat re-enqueues the event.

Accepted work is not lost.

GigaChat timeout, 429, 5xx, or rejected OAuth token

Typed errors, bounded jittered backoff, and controlled token refresh behind a Redis lock.

After the retry limit, the assistant falls back and escalates.

Temporary Telegram Bot API error

The Outbox delivery returns to retrying with exponential backoff.

The business reply remains persisted independently.

Medical, sensitive, or clearly unusual enquiry

The conversation becomes escalated, AI replies stop, and the artist is notified.

The decision remains with a person.

OPERATOR EXPERIENCE

The backend ends in a usable studio workflow, not just a bot reply.

The built-in workspace lets the artist review conversations, reply to clients, maintain CRM context, create separate tattoo applications, and manage appointments without a separate frontend application.

01

Conversations and handoff

An enquiry queue, unread counters, takeover, return to AI, manual replies through Outbox, and conversation closure.

02

CRM and applications

Idea, placement, size, style, budget, preferred date, references, and an immutable history of funnel transitions.

03

Appointments and deposits

Sessions, agreed price, deposit states, and PostgreSQL constraints that prevent overlapping active appointments.

Validated application funnel
  1. new
  2. qualification
  3. consultation
  4. awaiting_deposit
  5. booked
  6. completed
TESTING & DELIVERY

The checks cover concurrent invariants, not only isolated functions.

CI is split into independent layers: fast unit checks, a PostgreSQL integration environment, and production Docker image validation from the lock file.

UNITPython 3.11 / 3.12

Ruff, Alembic graph checks, the unit suite, and the current 74% coverage gate. The regression dataset contains 169 labelled client phrases.

INTEGRATIONPostgreSQL 15 + pgvector

Migrations from an empty database and concurrent Inbox/Outbox, uniqueness, and health endpoint tests.

CONTAINERDocker Compose

Production and development Compose validation, an image built strictly from uv.lock, and import/config smoke tests.

TRADE-OFFS & NEXT

The boundaries of the guarantees are explicit.

The case study does not present the system as a finished enterprise platform. The repository documents current limitations and the next level of production validation.

Known trade-off

At-least-once Telegram delivery

The Bot API does not accept an idempotency key for sendMessage. A rare crash after a successful send but before committing sent state can duplicate a platform message.

In progress

Strict type checking

Mypy remains a diagnostic stage while accumulated type debt is being reduced rather than a blocking quality gate.

Roadmap

Production observability

Structured logs and health endpoints exist; metrics, dashboards, load, end-to-end, and chaos tests remain on the roadmap.

RESULT & LESSONS

A Telegram bot evolved into an event-driven product backend.

The project connects transport, background work, AI, client data, and the operator's real workflow. Its value is not the number of technologies but the explicit guarantees between them.

  • A reliable webhook begins with durable event acceptance, not task.delay().
  • AI fallback belongs in the business flow rather than a generic except block.
  • Human handoff works only when conversation ownership is persisted as state.
  • Booking and payment invariants are safer when PostgreSQL enforces them as well as the UI.
SOURCE & EVOLUTION

Code, migrations, tests, and decision history are available in the repository.

The project continues through verified iterations: each substantial patch passes the complete check suite and manual validation of critical behaviour.