Back to projects
In production

Dalem Conecta

CRM for wholesale and retail travel agencies in Colombia, live in production.

Role: Full-stack design and development

  • NestJS
  • Prisma
  • PostgreSQL
  • Angular
  • Ionic
  • Capacitor
Dalem Conecta dashboard

Problem

Travel agencies in Colombia often run their operation across spreadsheets, chats and the memory of whoever picks up the phone. A quote lives in one message, the sale in another file and the to-dos in someone’s head. When the team grows, or when the advisor who knew everything is on vacation, information gets lost and so do sales.

Tracking payments is another weak spot. A trip is paid in installments, sometimes in two currencies, and every payment has to be tied to the right sale. If that record lives in a loose spreadsheet, nobody knows for sure how much is still owed or when the next installment is due. And after the trip, after-sales is almost always forgotten: there is no survey, incidents go unrecorded, and the customer does not hear from the agency again until they need another trip.

There is also a case that almost no generic tool handles well: wholesalers and retailers working together. The wholesaler builds packages and the retailer sells them to their own customers, and between the two there is a flow of orders, seat holds and payments that is still coordinated by hand, through messages and phone calls.

I wanted a single place where an agency could run the whole trip, from the first quote to after-sales, without one agency ever being able to see another’s data. That last condition shaped almost every technical decision that follows.

Solution and key features

Dalem Conecta is a CRM built around that full cycle. An agency signs in from the browser or from the Android app and always works with the same data. These are the main pieces:

Each agency has its own team with different roles, and an administrator can onboard new agencies. The product is in production and I keep improving it based on what real use teaches. Every architecture decision in the sections below came from a concrete problem that needed solving, so I tell them layer by layer: first the API, then the client, and finally the site that presents the product.

API

The API is built with NestJS and Prisma on top of PostgreSQL. I chose NestJS because its module structure forces order on the code from day one, which matters when a single person maintains the whole backend and has to come back to a module after months without touching it. Prisma gives me a typed schema and versioned migrations, so the data-access code stays short and readable.

The central decision is multi-tenancy. All agencies share one database, and the isolation between them is applied automatically. Every request carries the context of the agency that makes it, using nestjs-cls to hold it for the whole life of the request, and a Prisma extension adds the agency filter to every query without the developer having to remember to write it. That greatly reduces the risk of forgetting a filter in a new query, which is the classic mistake in this kind of system.

Because an automatic filter is still code, and code can fail, I added a second barrier inside the database itself: PostgreSQL row-level security (RLS). If a query ever slips past the application filter, the database refuses to return rows from another agency. These are two independent layers, and the redundancy is deliberate. For a product where one agency must never see another’s data, I am happy to pay the cost of maintaining both.

Authentication uses JWT with refresh tokens and per-agency roles. The access token is short-lived and the refresh token is rotated on every use, so a stolen token has a very narrow window. Roles separate what an administrator, a sales advisor and the operations team can do, and the system enforces those rules on the server, never only in the interface. There is also a limit on sign-in attempts and a separate case for the superadmin, who can switch agencies to give support.

Finally, the API publishes an OpenAPI specification that works as a contract. It is more than documentation: the typed client used by the interface comes from it, as I explain in the next section. Per-agency modules are controlled by a global guard, so a feature that is switched off for an agency does not respond even if someone knows the route.

Client

The client is built with Angular, Ionic and Capacitor. It is a single codebase for the web and for the Android app, and that is the decision that saves me the most time. One person maintains both clients, with the same screens and the same logic, and every improvement reaches both at once. Today the product runs in the browser and on Android, which is what the agencies need, and I would rather say so plainly than promise platforms that do not exist.

The price of that choice is that the app is not purely native. On very demanding screens I give up some smoothness, but in exchange I maintain far less code, and for a CRM made of forms, lists and dashboards that cost is barely noticeable.

The types and the HTTP client are generated from the API’s OpenAPI specification. That way the contract between client and server lives in one place, and an API change that breaks a screen is caught at compile time, not in production. The cost is an extra generation step in the workflow and the discipline of keeping the specification up to date, which I accept without hesitation.

Two features show how the work is split between client and server. The first is the quote PDF: it is generated in the browser, not on the server, which keeps load off the API and keeps the flow fast. I had to design it carefully so the document looks the same on any device, because the result depends on the advisor’s machine. The second is the WhatsApp templates: agencies already close their sales on that channel, so from the quote the advisor sends the PDF with a message built from a template, and can adjust it before sending.

That split follows a simple rule: the API returns raw data and the client handles presentation, meaning the texts, the currency and date formats, and the documents. The exception is email, which goes out from the server with templates specific to each agency.

Marketing website

The product also has a static website that presents it to interested agencies, at dalemconecta.com, separate from the application that lives at app.dalemconecta.com. I keep it apart on purpose: it is fast, it does not depend on the API, and its copy or design can change without touching the product. I designed and built it too, with the same goal of loading fast and being understood in a few seconds.

See the website case →

Architecture

Dalem Conecta architectureSimplified diagram: the web and Android client calls the API; the API uses the database, authentication and the external email and WhatsApp services. The marketing site is independent.Web and AndroidAngular + IonicAPINestJS + PrismaPer-tenant RLSExternal servicesEmail and WhatsAppMarketing siteStatic AstroDatabasePostgreSQLAuthenticationJWT + rolesDalem Conecta architectureSimplified diagram: the web and Android client calls the API; the API uses the database, authentication and the external email and WhatsApp services. The marketing site is independent.Web and AndroidAngular + IonicAPINestJS + PrismaPer-tenant RLSExternal servicesEmail and WhatsAppMarketing siteStatic AstroDatabasePostgreSQLAuthenticationJWT + roles

Simplified diagram; internal details omitted for privacy.

Technical decisions

  1. Several agencies share a single database, with isolation applied automatically.

    Why
    Every query is filtered by agency without the developer having to remember it, and PostgreSQL row-level security acts as a second barrier if anything slips through.
    Trade-off
    A bug in the automatic filter would be more serious than with separate databases, so that isolation needs more rigorous testing.
  2. One Angular codebase with Ionic and Capacitor for both the web app and Android.

    Why
    A single person maintains both clients with the same screens and the same logic, and every improvement reaches both at once.
    Trade-off
    The app is not purely native: I give up some smoothness on very demanding screens in exchange for far less code to maintain.
  3. Client types are generated from the API's OpenAPI specification.

    Why
    The contract between client and server lives in one place, and an API change that breaks a screen is caught at compile time, not in production.
    Trade-off
    It adds a generation step to the workflow and means keeping the specification up to date.
  4. The quote PDF is generated in the browser and sent through a WhatsApp template.

    Why
    Agencies already close their sales over WhatsApp, and generating the PDF on the client keeps load off the server and the flow fast.
    Trade-off
    The result depends on the advisor's device, so I had to design the PDF carefully to look the same everywhere.

My role

I designed and built the whole product: the API, the web and Android client, and the marketing site. I made the architecture decisions, wrote the code and shipped the system to production. Today I maintain it and keep growing it based on what real usage shows.

Next project

In productionDalem Conecta websiteStatic website that introduces Dalem Conecta to travel agencies. Fast, optimized for search and in production.View case study

Have a project?

Let's talk about your project