<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Databases | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/databases/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:51 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/databases/index.xml" rel="self" type="application/rss+xml"/><item><title>Is RAG Dead? Long Context, Grep, and the End of the Mandatory Vector DB</title><link>https://akitaonrails.github.io/en/2026/04/06/rag-is-dead-long-context/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/04/06/rag-is-dead-long-context/</guid><pubDate>Mon, 06 Apr 2026 11:00:00 -0300</pubDate><description>&lt;p&gt;This is one of those itches I can&amp;rsquo;t scratch. Back in the early LLM days, around 2022/2023, we had 4k of context on GPT 3.5, 8k if you were lucky, 32k was a luxury. To do anything with a real document you had no choice: chop the text into pieces, generate embeddings, throw them in a vector database, do similarity search, grab the top-5 chunks, and pray the right ones came back.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This is one of those itches I can&rsquo;t scratch. Back in the early LLM days, around 2022/2023, we had 4k of context on GPT 3.5, 8k if you were lucky, 32k was a luxury. To do anything with a real document you had no choice: chop the text into pieces, generate embeddings, throw them in a vector database, do similarity search, grab the top-5 chunks, and pray the right ones came back.</p>
<p>Then it became an industry. Pinecone, Weaviate, Qdrant, Chroma, Milvus, pgvector, LangChain, LlamaIndex, Haystack. Tutorials everywhere, &ldquo;build your chatbot with your PDFs,&rdquo; entire consultancies feeding off this. It became the &ldquo;hello world&rdquo; of applied LLMs: document → chunk → embed → vector DB → query.</p>
<p>Today, in April 2026, Claude Opus 4.6 has 1 million tokens of context. Sonnet 4.6 too. Gemini 3.1 Pro too. GPT 5.4 has a smaller window but still in the comfortable range, in the hundreds of thousands. And some models already have experimental 2M token modes. The question that keeps nagging at me: what on earth do I need a vector stack for, to solve a problem that fits inside the model&rsquo;s window?</p>
<p>And there&rsquo;s more: vector databases have real problems nobody wants to talk about. False neighbors. Arbitrary chunking that splits a definition from its usage. Embeddings that age badly. Not to mention that when the result is wrong, you have absolutely no idea why.</p>
<p>The thesis I&rsquo;ve been chewing on is simple: in most cases, a well-aimed <code>grep</code> plus a generous context window beats a full RAG stack. It&rsquo;s cheaper, it&rsquo;s easier to maintain, and when it breaks you can actually debug it. Let&rsquo;s break this down.</p>
<h2>What the Claude Code leak showed<span class="hx:absolute hx:-mt-20" id="what-the-claude-code-leak-showed"></span>
    <a href="#what-the-claude-code-leak-showed" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before we get into the theory, let me bring up something that happened a few days ago that backs this whole argument up. On March 31, 2026, Anthropic, by accident, published version 2.1.88 of the <code>@anthropic-ai/claude-code</code> package on npm with a nearly 60 MB source map attached, and roughly 512,000 lines of TypeScript from their internal tool leaked into the wild. I already <a href="/en/2026/03/31/claude-code-source-code-leaked-what-we-found-inside/">wrote about the incident last week</a>, with more detail on what showed up in the code.</p>
<p>The part that matters for this discussion is Claude Code&rsquo;s memory system. Instead of dumping everything into a vector DB, the architecture has three layers. There&rsquo;s a <code>MEMORY.md</code> that stays permanently loaded in context, but it doesn&rsquo;t hold any actual data: it&rsquo;s just an index of pointers, around 150 characters per line, kept under 200 lines and about 25 KB. The real facts live in &ldquo;topic files&rdquo; that get pulled on demand when the agent needs them. And the raw transcripts from previous sessions are never reloaded whole, only searched with grep, hunting for specific identifiers. No embedding. No Pinecone. Just write discipline (topic file first, index after) and lexical search. That&rsquo;s it.</p>
<p>Claude Code&rsquo;s main loop also has a tiered system for handling a context that&rsquo;s filling up. As <a href="/en/2026/03/31/claude-code-source-code-leaked-what-we-found-inside/">I detailed in the previous post</a>, there are five different context compaction strategies, with names like <code>microcompact</code> (clears old tool results based on age), <code>context collapse</code> (summarizes long stretches of conversation), and <code>autocompact</code> (which fires when the context gets close to the limit). The CLAUDE.md file, which a lot of people thought was just a convention, is first-class in the architecture: the system re-reads it at every iteration of the query.</p>
<p>What this tells me: the best coding agent on the market right now, built by the company selling the most expensive model out there, <strong>does not use a vector DB</strong>. It uses files on disk, a markdown index, lexical search, and smart compaction strategies for when the context overflows. They could&rsquo;ve slapped embeddings on top, they have the money to run whatever they wanted, and they chose not to. The reason, in my reading, is exactly what this post is arguing: to retrieve text from files you control, with generous context available, a vector DB is dead weight. Better to invest in compacting the window you already have than indexing everything into an external store.</p>
<p>There&rsquo;s a curious security detail that came along with the leak: people noticed the compaction pipeline has a vulnerability they&rsquo;re calling &ldquo;context poisoning.&rdquo; Content that looks like an instruction, coming from a file the model reads (say, a CLAUDE.md from a cloned repo), can end up being preserved by the compaction model as if it were &ldquo;user feedback,&rdquo; and the next model takes that as a real user instruction. It&rsquo;s a new attack vector. But that&rsquo;s a topic for another post.</p>
<h3>The &ldquo;Dream&rdquo; system and memory consolidation<span class="hx:absolute hx:-mt-20" id="the-dream-system-and-memory-consolidation"></span>
    <a href="#the-dream-system-and-memory-consolidation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>But what really caught my eye for the RAG debate, which I <a href="/en/2026/03/31/claude-code-source-code-leaked-what-we-found-inside/">unpacked in detail last week</a>, is the system called <code>autoDream</code>. It&rsquo;s a forked subagent, with read-only bash access to the project, that runs in the background while you&rsquo;re not using the tool. Its job is literally to dream: to consolidate memory. The name isn&rsquo;t accidental, and the obvious analogy (which I couldn&rsquo;t resist) is the human brain consolidating memory during sleep, turning short-term experience into something more stable.</p>
<p>For a dream to actually run, three gates have to open at once: 24 hours since the last dream, at least 5 sessions since the last dream, and a consolidation lock that prevents concurrent dreams. When it fires, it goes through four phases. Orient (does an <code>ls</code> on the memory directory, reads the index). Gather (looks for new signals in logs, stale memories, transcripts). Consolidate (writes or updates the topic files, converts relative dates into absolute ones, deletes facts that have been contradicted). And Prune, the final cleanup that keeps the index under 200 lines.</p>
<p>The decision to make <code>autoDream</code> a forked subagent is the detail that matters here. It does not run in the same loop as the main agent. Why? Because memory consolidation is a noisy process. The model has to re-read old transcripts, compare them against what&rsquo;s in <code>MEMORY.md</code>, decide what stays and what goes, form hypotheses about things it saw in earlier sessions. If that ran in the main context, it would pollute the &ldquo;train of thought&rdquo; of the agent that&rsquo;s trying to help you with your current task. By forking, you keep the two separate. The main agent stays focused on what you asked for, and <code>autoDream</code> does the housekeeping in parallel, with no write permission on the project.</p>
<p>And the way it figures out what needs to be consolidated is plain old lexical search. The transcripts live as JSONL files on disk, and <code>autoDream</code> uses grep to look for new signals. Just grep, on text logs. Stop and think about that for a second. The memory consolidation of the most advanced agent in the world, built by one of the richest AI companies out there, is a forked subagent running grep on text logs. If a vector DB were the right answer for this kind of problem, Anthropic would&rsquo;ve put a vector DB in there. They didn&rsquo;t.</p>
<p>And there&rsquo;s a detail that, to me, is the buried gold of the entire leak, and it fits this argument like a glove. In <code>autoDream</code>, memory is treated as a hint. The system assumes that what&rsquo;s stored may be stale, wrong, contradicted by something that happened later, and the model has to verify before it trusts it. The vector DB pitch is the opposite of that: index everything, search by similarity, return the top-k, trust the result. Claude Code went the conservative route. Index little, search by word, return a hint, and stay skeptical until you&rsquo;ve laid eyes on the actual fact.</p>
<p>The whole strategy works in two layers. Inside a single session: generous context plus grep plus smart compaction (<code>microcompact</code>, <code>context collapse</code>, <code>autocompact</code>). Between sessions: a subagent that consolidates memory asynchronously, using grep on the transcripts and treating the result as a tip, not as truth. Embeddings and vector DBs don&rsquo;t show up in either layer. The deliberate choice was a smart reader chewing on raw text, not a dumb reader being spoonfed the top-k of an embedding.</p>
<p>The practical lesson for our debate is simple. The most advanced agents on the market are heading toward generous context, lexical search, and smart compaction, not toward classic RAG pipelines. If Anthropic, with all the infrastructure and talent they&rsquo;ve got, picked this path for Claude Code, those of us building internal applications on a fraction of that budget should at least think about going the same way.</p>
<h2>Where the story started turning<span class="hx:absolute hx:-mt-20" id="where-the-story-started-turning"></span>
    <a href="#where-the-story-started-turning" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When the ceiling was 32k of context, retrieval was the bottleneck of the entire problem. You had to pre-filter aggressively, because anything that made it into the window was sacred space. A vector DB was the only halfway-decent way to do that semantic pre-filtering. The logic was: &ldquo;the reader (LLM) is expensive and dumb, so the retriever has to be smart and selective.&rdquo;</p>
<p>Today the equation has flipped. The reader is now the smartest one at the table, and the window grew big enough to hold an entire document. So the retriever can (and maybe should) go back to being dumb. The dumber, the better. You want high recall and low precision, and you let the model do the fine work. Grep does exactly that. So does BM25. And ripgrep flies through millions of lines without breaking a sweat.</p>
<p>And this isn&rsquo;t just my hunch. The BEIR benchmarks have shown for a while now that BM25 matches or beats a lot of dense retrievers when the domain drifts away from where the embeddings were trained. Anthropic itself published a post on <a href="https://www.anthropic.com/news/contextual-retrieval"target="_blank" rel="noopener">Contextual Retrieval</a> that basically says the same thing: a lexical signal plus an LLM&rsquo;s judgment beats pure embeddings on most knowledge tasks. And take a look at Claude Code, the tool I&rsquo;ve been using every day for 500 hours: it navigates the repo with <code>Glob</code> and <code>Grep</code>. No vector DB, no embedding, no LangChain. It works ridiculously well.</p>
<h2>The real problems with vector databases nobody advertises<span class="hx:absolute hx:-mt-20" id="the-real-problems-with-vector-databases-nobody-advertises"></span>
    <a href="#the-real-problems-with-vector-databases-nobody-advertises" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The vector DB marketing sells the dream of perfect semantic search. Reality is messier.</p>
<p>False neighbors come first. Cosine similarity rewards topical similarity, not relevance. You ask &ldquo;how do we handle authentication errors&rdquo; and the DB returns every chunk that mentions authentication. The chunk that actually answers the question may be in tenth place, or may not have been retrieved at all because the doc author wrote &ldquo;login&rdquo; instead of &ldquo;auth.&rdquo;</p>
<p>Chunking is the second one, and it&rsquo;s a disguised disaster. A 512-token window with a 64-token overlap sounds reasonable, until you realize your important table got cut in half, the function definition ended up separated from its usage, and the piece of documentation with the exact command got orphaned without the context of its section. The chunk boundary tends to land exactly where the answer was living.</p>
<p>When it fails, it fails without leaving a trace. When BM25 misses, you know why: the word isn&rsquo;t there. When a vector DB returns garbage, you get a plausible-looking wrong chunk, with no diagnostic signal at all. Good luck debugging that in production at two in the morning.</p>
<p>The index gets stale. Every document update calls for re-embedding. If you have 10,000 docs and 200 of them change per day, that turns into a batch process, monitoring, a queue, retries, embedding API costs, and an unavoidable inconsistency window between what&rsquo;s on disk and what&rsquo;s in the index. Grep has none of that. File changed? The next query already sees it.</p>
<p>And there&rsquo;s the operating cost nobody adds up. Pinecone charges per vector. Weaviate wants a cluster to maintain. pgvector saves you a new server but you still own a schema, an index, and a re-embedding pipeline. Each of those things wants engineer time, monitoring, tests, deploys. All of that to do a search that <code>rg</code> would often crack in 200ms.</p>
<h2>Comparing the complexity<span class="hx:absolute hx:-mt-20" id="comparing-the-complexity"></span>
    <a href="#comparing-the-complexity" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Look at the diagram:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/06/rag/rag-vs-grep-complexity.png" alt="Complexity: classic RAG vs grep &#43; long context"  loading="lazy" /></p>
<p>On one side, eight steps, four or five services, an external index that needs to be maintained and kept up to date. On the other, four steps, zero new infrastructure. This isn&rsquo;t a caricature: it is literally what you have to set up for each case.</p>
<p>The honest question: does the left column pay off? In 2023, yes, because the right column didn&rsquo;t exist (no LLM had a 200k window). In 2026, in most cases, it doesn&rsquo;t.</p>
<h2>Pros and cons of each side<span class="hx:absolute hx:-mt-20" id="pros-and-cons-of-each-side"></span>
    <a href="#pros-and-cons-of-each-side" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Classic RAG (vector DB)<span class="hx:absolute hx:-mt-20" id="classic-rag-vector-db"></span>
    <a href="#classic-rag-vector-db" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>For:</strong></p>
<ul>
<li>Works for huge document bases, on the order of hundreds of GB, where even <code>rg</code> won&rsquo;t cut it without prior indexing</li>
<li>Handles heavy paraphrase and cross-lingual queries (&ldquo;how do I cancel&rdquo; vs. &ldquo;subscription termination process&rdquo;) where the user&rsquo;s vocabulary doesn&rsquo;t match the document&rsquo;s</li>
<li>Works for non-textual modalities (image, audio) where grep has nothing to look at</li>
<li>Saves input tokens if you&rsquo;re tight on budget or absolute latency</li>
</ul>
<p><strong>Against:</strong></p>
<ul>
<li>Complex stack: embedding, vector DB, chunking, reranker, re-indexing pipeline</li>
<li>Opaque failures, hard to debug</li>
<li>Chunking destroys the context of tables, code, long definitions</li>
<li>Operational overhead (index, queue, monitoring, re-embedding cost)</li>
<li>The semantic search the marketing is selling rarely works the way the marketing promises</li>
</ul>
<h3>Grep + long context<span class="hx:absolute hx:-mt-20" id="grep--long-context"></span>
    <a href="#grep--long-context" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>For:</strong></p>
<ul>
<li>Practically zero new infrastructure: ripgrep, sqlite, or a plain <code>LIKE</code> in Postgres</li>
<li>Always fresh: file changes, the next query sees them</li>
<li>Transparent failures: the word is either there or it isn&rsquo;t</li>
<li>Loads the document in generous chunks, the model does the fine filtering with actual semantics</li>
<li>Cheaper in dev and ops, cheaper to pivot domains</li>
</ul>
<p><strong>Against:</strong></p>
<ul>
<li>Doesn&rsquo;t scale to terabytes of raw text without some kind of indexing</li>
<li>Suffers when the user&rsquo;s vocabulary is very different from the document&rsquo;s</li>
<li>Doesn&rsquo;t work for non-textual modalities</li>
<li>Per-query latency is higher in absolute terms (loading 100k tokens always costs more than loading 5k)</li>
<li>Per-query input cost is higher if you don&rsquo;t have prompt caching</li>
</ul>
<h2>But what about cost?<span class="hx:absolute hx:-mt-20" id="but-what-about-cost"></span>
    <a href="#but-what-about-cost" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is the argument I get hit with the most when I defend the &ldquo;load everything into context&rdquo; thesis. &ldquo;It&rsquo;ll get crazy expensive, 200k tokens of input per query is absurd.&rdquo; Let&rsquo;s actually run the numbers.</p>
<p>In <a href="/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/">yesterday&rsquo;s LLM benchmark post</a> I mapped out the per-token price of every model. Take Claude Sonnet 4.6: $3 per million input tokens, $15 per million output. Take GLM 5 (which I proved actually works): $0.60 input, $2.20 output. Take GPT 5.4 Pro at the top of the heap: $15 input, $180 output (yeah, that one stings, I know).</p>
<p>Before we turn &ldquo;200k tokens&rdquo; into dollars, let&rsquo;s land that number on something tangible, because &ldquo;100k tokens&rdquo; doesn&rsquo;t mean anything to anyone. A token, on average, is roughly 0.75 of a word in English (Portuguese is similar, maybe a touch heavier because of longer words). So, translating:</p>
<ul>
<li><strong>100k tokens</strong> ≈ 75,000 words ≈ a whole short novel like Hemingway&rsquo;s <em>The Old Man and the Sea</em> with room to spare, or about three long Wikipedia articles glued together.</li>
<li><strong>200k tokens</strong> ≈ 150,000 words ≈ a big novel, like <em>Crime and Punishment</em> in full, or half of the first <em>Game of Thrones</em> book (which clocks in around 298k words, so roughly 400k tokens).</li>
<li><strong>400k tokens</strong> ≈ 300,000 words ≈ <em>A Game of Thrones</em> in full, the entire first book of the series in your window.</li>
<li><strong>1M tokens</strong> ≈ 750,000 words ≈ the entire <em>Lord of the Rings</em> trilogy plus <em>The Hobbit</em>, or the whole Bible (King James is around 783k words, roughly 1M tokens), or about two and a half <em>Game of Thrones</em> books stacked on top of each other.</li>
</ul>
<p>So when I say &ldquo;throw 200k tokens of input at the model,&rdquo; what that actually means in the real world is &ldquo;throw the entire <em>Crime and Punishment</em> in as the context for your question.&rdquo; That&rsquo;s a lot. And that&rsquo;s exactly what makes the argument of this post viable: today&rsquo;s models can read an entire novel in one go and still answer a specific question about it. In 2023, this was science fiction. In 2026, it&rsquo;s the base case.</p>
<p>So picture a query that throws 200k tokens of input at the model (there goes <em>Crime and Punishment</em> again) and produces 2k tokens of output (about three pages of response):</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Input ($)</th>
          <th style="text-align: right">Output ($)</th>
          <th style="text-align: right">Total per query</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: right">$0.60</td>
          <td style="text-align: right">$0.03</td>
          <td style="text-align: right"><strong>$0.63</strong></td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: right">$3.00</td>
          <td style="text-align: right">$0.15</td>
          <td style="text-align: right"><strong>$3.15</strong></td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: right">$0.12</td>
          <td style="text-align: right">$0.0044</td>
          <td style="text-align: right"><strong>$0.12</strong></td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: right">$0.40</td>
          <td style="text-align: right">$0.024</td>
          <td style="text-align: right"><strong>$0.42</strong></td>
      </tr>
      <tr>
          <td>GPT 5.4 Pro</td>
          <td style="text-align: right">$3.00</td>
          <td style="text-align: right">$0.36</td>
          <td style="text-align: right"><strong>$3.36</strong></td>
      </tr>
  </tbody>
</table>
<p>Now throw prompt caching into the mix. Claude has a cache that drops cached input to a fraction of the full price (in the ballpark of 10%, depending on the model). Gemini has a similar mechanism. When you fire a sequence of queries against the same 200k-token dump, the cost of subsequent queries plummets to pennies. With Sonnet cached, you can fairly call it about $0.10 per follow-up query without making things up.</p>
<p>Now compare that to the cost of running a Pinecone, or a Weaviate, or a pgvector. Setting aside the price of the subscription itself (which varies a lot), you need an engineer to wire up the pipeline, maintain it, monitor it, deal with embedding failures, redo the chunking when the domain shifts. Conservatively, you&rsquo;re looking at somewhere between 40 and 80 hours of engineering to make the thing stable. At R$ 200/hour, that&rsquo;s between R$ 8,000 and R$ 16,000. In USD, somewhere between $1,600 and $3,200 just to stand it up.</p>
<p>With $3,200, on Sonnet 4.6 with prompt caching, you can run something on the order of 30,000 queries of 200k tokens each. Thirty thousand queries, depending on the scale of the project, gives you several months or even an entire year of an average internal tool. And you didn&rsquo;t pay an engineer to wire up a pipeline. There&rsquo;s no vector DB server to maintain. And if the document changes, the system already sees it on the next query.</p>
<p>The &ldquo;RAG is cheaper in tokens&rdquo; argument ignores that tokens are the cheapest thing in the entire equation. Engineers cost a lot, servers cost a lot, bugs in production cost a whole lot more. Tokens have become a commodity, and they&rsquo;re getting cheaper with every new model release.</p>
<p>The classic RAG argument was &ldquo;the model is expensive, retrieval is cheap.&rdquo; Today it&rsquo;s the opposite: the model is the cheap part of the stack, smart retrieval is what costs a fortune to build and maintain.</p>
<h2>Where the thesis doesn&rsquo;t hold<span class="hx:absolute hx:-mt-20" id="where-the-thesis-doesnt-hold"></span>
    <a href="#where-the-thesis-doesnt-hold" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I don&rsquo;t want to come off as a fanboy. There are cases where classic RAG still wins:</p>
<ol>
<li><strong>Massive corpora.</strong> If you have 500 GB of raw text, even <code>rg</code> won&rsquo;t solve it in acceptable time. You need some kind of indexing. It can be indexed BM25 (Tantivy, Elasticsearch), it can be a vector DB. But notice: the first option is still lexical, not vector.</li>
<li><strong>Wildly scattered vocabulary.</strong> Customer support, where the user types &ldquo;my wifi&rsquo;s down&rdquo; and the documentation says &ldquo;loss of connectivity at the physical layer.&rdquo; BM25 won&rsquo;t catch that. Embedding will. Vector DB scores a point here.</li>
<li><strong>Non-textual modalities.</strong> Image-by-image search, audio-by-audio. Embedding is mandatory.</li>
<li><strong>Critical absolute latency.</strong> If you have to answer in 100ms with a 5k input budget, a generous dump won&rsquo;t fit. Pre-filtering is necessary.</li>
<li><strong>Compliance and audit.</strong> If you have to prove that a specific document was consulted to answer a specific query, having indexed and trackable chunks helps. A 200k-token context dump is more opaque from an audit standpoint.</li>
</ol>
<p>For those cases, classic RAG still makes sense. But notice the size of the list. These are specific cases. The general case, things like &ldquo;chat with our internal docs&rdquo; or &ldquo;ask the product manual,&rdquo; almost all of it falls into the &ldquo;grep + long context handles it better&rdquo; bucket.</p>
<h2>Lazy retrieval: the recipe I&rsquo;d defend<span class="hx:absolute hx:-mt-20" id="lazy-retrieval-the-recipe-id-defend"></span>
    <a href="#lazy-retrieval-the-recipe-id-defend" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If I were building a &ldquo;chat with docs&rdquo; tool today, from scratch, it would look more or less like this:</p>
<ol>
<li><strong>Keep the documents raw.</strong> Markdown, converted PDF, code, whatever. On disk, organized in folders that make sense for the domain.</li>
<li><strong>Fast lexical filter.</strong> <code>ripgrep</code> with regex, or BM25 with Tantivy/SQLite FTS5, or a <code>LIKE</code> in Postgres if you already have one. Returns 100-300 hits.</li>
<li><strong>Load generously.</strong> Grab not just the matching snippet, but the entire file, or a wide window around it. Throw all of it into the context.</li>
<li><strong>Let the LLM do the fine work.</strong> Pass the original question, tell the model to find what matters, drop the rest, and answer with citations.</li>
<li><strong>(Optional) Add embeddings only for the query classes where lexical fails</strong>, after you have real data showing that it fails.</li>
</ol>
<p>This is the opposite of the old advice (&ldquo;start with vectors, fall back to keyword&rdquo;). It&rsquo;s: <strong>start with keyword, and add vector only if you feel the gap</strong>. In most projects, you never will.</p>
<h2>A toy implementation in Ruby<span class="hx:absolute hx:-mt-20" id="a-toy-implementation-in-ruby"></span>
    <a href="#a-toy-implementation-in-ruby" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To make it concrete. Here&rsquo;s a Ruby script using the <a href="https://github.com/crmne/ruby_llm"target="_blank" rel="noopener"><code>ruby_llm</code></a> gem (the same one from yesterday&rsquo;s benchmark) that does exactly this flow: grep through the files, load the snippets with context, send to Claude, get the answer back. No vector DB, no chunking, no embedding, no LangChain.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env ruby</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;ruby_llm&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;open3&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">DOCS_DIR</span> <span class="o">=</span> <span class="no">ARGV</span><span class="o">[</span><span class="mi">0</span><span class="o">]</span> <span class="o">||</span> <span class="s2">&#34;./docs&#34;</span>
</span></span><span class="line"><span class="cl"><span class="no">QUERY</span>    <span class="o">=</span> <span class="no">ARGV</span><span class="o">[</span><span class="mi">1</span><span class="o">]</span> <span class="ow">or</span> <span class="nb">abort</span> <span class="s2">&#34;uso: ./ask.rb &lt;pasta&gt; &lt;pergunta&gt;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Fast lexical filter with ripgrep.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    -i case insensitive, -l file names only, --type-add covers md/txt/extracted-pdf.</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">lexical_search</span><span class="p">(</span><span class="n">dir</span><span class="p">,</span> <span class="n">query</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">terms</span> <span class="o">=</span> <span class="n">query</span><span class="o">.</span><span class="n">downcase</span><span class="o">.</span><span class="n">scan</span><span class="p">(</span><span class="sr">/\w{4,}/</span><span class="p">)</span><span class="o">.</span><span class="n">uniq</span><span class="o">.</span><span class="n">first</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>  <span class="c1"># words with 4+ letters</span>
</span></span><span class="line"><span class="cl">  <span class="n">pattern</span> <span class="o">=</span> <span class="n">terms</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;|&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">cmd</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;rg&#34;</span><span class="p">,</span> <span class="s2">&#34;-l&#34;</span><span class="p">,</span> <span class="s2">&#34;-i&#34;</span><span class="p">,</span> <span class="s2">&#34;-e&#34;</span><span class="p">,</span> <span class="n">pattern</span><span class="p">,</span> <span class="n">dir</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="n">files</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="no">Open3</span><span class="o">.</span><span class="n">capture2</span><span class="p">(</span><span class="o">*</span><span class="n">cmd</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">files</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">reject</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:empty?</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Load entire files (up to a reasonable cap).</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">load_context</span><span class="p">(</span><span class="n">files</span><span class="p">,</span> <span class="ss">max_chars</span><span class="p">:</span> <span class="mi">600_000</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">total</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">  <span class="n">files</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">body</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">if</span> <span class="n">total</span> <span class="o">+</span> <span class="n">body</span><span class="o">.</span><span class="n">size</span> <span class="o">&gt;</span> <span class="n">max_chars</span>
</span></span><span class="line"><span class="cl">    <span class="n">total</span> <span class="o">+=</span> <span class="n">body</span><span class="o">.</span><span class="n">size</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;## </span><span class="si">#{</span><span class="n">path</span><span class="si">}</span><span class="se">\n\n</span><span class="si">#{</span><span class="n">body</span><span class="si">}</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span><span class="o">.</span><span class="n">compact</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">---</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Send to Claude with the question and the documents.</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">ask</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="s2">&#34;anthropic/claude-sonnet-4-6&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">prompt</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">PROMPT</span>
</span></span><span class="line"><span class="cl">    <span class="no">Você</span> <span class="n">tem</span> <span class="n">acesso</span> <span class="n">aos</span> <span class="n">documentos</span> <span class="n">abaixo</span><span class="o">.</span> <span class="no">Responda</span> <span class="n">a</span> <span class="n">pergunta</span> <span class="k">do</span> <span class="n">usuário</span>
</span></span><span class="line"><span class="cl">    <span class="n">usando</span> <span class="n">apenas</span> <span class="n">o</span> <span class="n">que</span> <span class="n">está</span> <span class="n">nos</span> <span class="n">documentos</span><span class="o">.</span> <span class="no">Cite</span> <span class="n">o</span> <span class="n">nome</span> <span class="k">do</span> <span class="n">arquivo</span> <span class="n">nas</span>
</span></span><span class="line"><span class="cl">    <span class="n">referências</span><span class="o">.</span> <span class="no">Se</span> <span class="n">a</span> <span class="n">resposta</span> <span class="n">não</span> <span class="n">estiver</span> <span class="n">nos</span> <span class="n">documentos</span><span class="p">,</span> <span class="n">diga</span> <span class="n">isso</span><span class="o">.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="o">---</span> <span class="no">DOCUMENTOS</span> <span class="o">---</span>
</span></span><span class="line"><span class="cl">    <span class="c1">#{context}</span>
</span></span><span class="line"><span class="cl">    <span class="o">---</span> <span class="no">FIM</span> <span class="no">DOS</span> <span class="no">DOCUMENTOS</span> <span class="o">---</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="ss">Pergunta</span><span class="p">:</span> <span class="c1">#{query}</span>
</span></span><span class="line"><span class="cl">  <span class="no">PROMPT</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span><span class="o">.</span><span class="n">content</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">files</span> <span class="o">=</span> <span class="n">lexical_search</span><span class="p">(</span><span class="no">DOCS_DIR</span><span class="p">,</span> <span class="no">QUERY</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">abort</span> <span class="s2">&#34;nenhum arquivo bateu&#34;</span> <span class="k">if</span> <span class="n">files</span><span class="o">.</span><span class="n">empty?</span>
</span></span><span class="line"><span class="cl"><span class="nb">puts</span> <span class="s2">&#34;Encontrei </span><span class="si">#{</span><span class="n">files</span><span class="o">.</span><span class="n">size</span><span class="si">}</span><span class="s2"> arquivos. Carregando contexto...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">context</span> <span class="o">=</span> <span class="n">load_context</span><span class="p">(</span><span class="n">files</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">puts</span> <span class="n">ask</span><span class="p">(</span><span class="no">QUERY</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>About 40 lines. No Pinecone dependency, no vector schema, no re-indexing pipeline. You run it as <code>./ask.rb ./docs &quot;how do I configure the payment webhook&quot;</code> and that&rsquo;s it.</p>
<p>That example is one-shot. You run it, it answers, done. For a real chat, with multiple questions in a row over the same documents, the design changes. Instead of running <code>lexical_search</code> upfront and shoving everything into the context at once, you expose the search as a tool to the model. Then it&rsquo;s the agent that decides when it needs to pull more docs, what term to look for, which file is worth opening in full. That&rsquo;s how Claude Code actually works: <code>Glob</code>, <code>Grep</code> and <code>Read</code> are tools, and the model picks the sequence. <code>ruby_llm</code> supports tool calling, so you can do the same thing in Ruby. It looks something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;ruby_llm&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;open3&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">DOCS_DIR</span> <span class="o">=</span> <span class="s2">&#34;./docs&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">SearchFiles</span> <span class="o">&lt;</span> <span class="no">RubyLLM</span><span class="o">::</span><span class="no">Tool</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="s2">&#34;Procura arquivos cujo conteúdo casa com o padrão dado (regex). Retorna lista de paths.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:pattern</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Padrão regex pra busca lexical (case-insensitive)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="ss">pattern</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="n">out</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="no">Open3</span><span class="o">.</span><span class="n">capture2</span><span class="p">(</span><span class="s2">&#34;rg&#34;</span><span class="p">,</span> <span class="s2">&#34;-l&#34;</span><span class="p">,</span> <span class="s2">&#34;-i&#34;</span><span class="p">,</span> <span class="s2">&#34;-e&#34;</span><span class="p">,</span> <span class="n">pattern</span><span class="p">,</span> <span class="no">DOCS_DIR</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">out</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">reject</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:empty?</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ReadFile</span> <span class="o">&lt;</span> <span class="no">RubyLLM</span><span class="o">::</span><span class="no">Tool</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="s2">&#34;Lê o conteúdo completo de um arquivo do projeto.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:path</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Caminho relativo do arquivo&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="ss">path</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="no">File</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;erro: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="s2">&#34;anthropic/claude-sonnet-4-6&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">.</span><span class="n">with_tools</span><span class="p">(</span><span class="no">SearchFiles</span><span class="p">,</span> <span class="no">ReadFile</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">.</span><span class="n">with_instructions</span><span class="p">(</span><span class="o">&lt;&lt;~</span><span class="no">SYS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">              <span class="no">Você</span> <span class="n">responde</span> <span class="n">perguntas</span> <span class="n">sobre</span> <span class="n">os</span> <span class="n">documentos</span> <span class="n">em</span> <span class="c1">#{DOCS_DIR}.</span>
</span></span><span class="line"><span class="cl">              <span class="no">Use</span> <span class="n">search_files</span> <span class="n">pra</span> <span class="n">encontrar</span> <span class="n">arquivos</span> <span class="n">relevantes</span> <span class="n">e</span> <span class="n">read_file</span>
</span></span><span class="line"><span class="cl">              <span class="n">pra</span> <span class="n">ler</span> <span class="n">o</span> <span class="n">conteúdo</span><span class="o">.</span> <span class="no">Sempre</span> <span class="n">cite</span> <span class="n">o</span> <span class="n">arquivo</span> <span class="n">na</span> <span class="n">resposta</span><span class="o">.</span>
</span></span><span class="line"><span class="cl">            <span class="no">SYS</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kp">loop</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">print</span> <span class="s2">&#34;&gt; &#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">msg</span> <span class="o">=</span> <span class="nb">gets</span><span class="o">&amp;.</span><span class="n">chomp</span>
</span></span><span class="line"><span class="cl">  <span class="k">break</span> <span class="k">if</span> <span class="n">msg</span><span class="o">.</span><span class="n">nil?</span> <span class="o">||</span> <span class="n">msg</span><span class="o">.</span><span class="n">empty?</span>
</span></span><span class="line"><span class="cl">  <span class="nb">puts</span> <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span><span class="o">.</span><span class="n">content</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The model gets the question, decides whether it needs to search, calls <code>search_files</code>, sees what came back, decides whether it needs to open any file, calls <code>read_file</code>, and only then answers. On the next question it already has the previous context in the session and can ask for more if it needs to. The context only receives what the model asked for, not the whole grep dump from the earlier example.</p>
<p>The same idea works for databases: swap <code>rg</code> for a SQL query with <code>LIKE</code> or <code>tsvector</code> (Postgres full-text), load the relevant rows, throw them in the context. If you have 10k records in an internal database, this handles it. If you have 10 million, you start needing smarter pagination or a more serious pre-filtering layer. But the mental model is the same: <strong>dumb filter + smart reader</strong>.</p>
<h2>The point that matters<span class="hx:absolute hx:-mt-20" id="the-point-that-matters"></span>
    <a href="#the-point-that-matters" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The most interesting thing in all of this isn&rsquo;t even the Pinecone savings. It&rsquo;s that the nature of the bottleneck has changed. In 2023, the bottleneck was retrieval: the reader was small, slow, expensive, and you needed a clever retriever to fill the window with the bare minimum. In 2026, the bottleneck is reasoning over messy context: the reader is big, relatively fast, and cheap. So it makes more sense to have a dumb retriever with high recall and let the model do the heavy lifting.</p>
<p>Anyone still designing systems with the 2023 mindset is paying a premium to solve a problem whose shape has changed. RAG didn&rsquo;t die, the &ldquo;R&rdquo; got dumber and cheaper, and that&rsquo;s an upgrade. The vector DB vendors aren&rsquo;t going to tell you this, but it&rsquo;s the path the more experienced folks have been quietly walking.</p>
<p>The next wave of LLM applications, in my bet, is going to be dominated by the people who got this inversion. Smaller stacks, simpler infrastructure, generous context, and a whole lot less LangChain.</p>
<h2>What the recent literature says<span class="hx:absolute hx:-mt-20" id="what-the-recent-literature-says"></span>
    <a href="#what-the-recent-literature-says" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before I close out, I went and checked what the research crowd published on this. Blog hot takes age in three months in this field, so it&rsquo;s better to look at the papers.</p>
<p><a href="https://arxiv.org/abs/2407.16833"target="_blank" rel="noopener"><strong>Retrieval Augmented Generation or Long-Context LLMs?</strong></a>, out of Google DeepMind, published at EMNLP 2024, is probably the most cited piece in the debate. Their conclusion: when the model has enough resources, long context beats RAG on average quality, but RAG is still much cheaper in tokens. They propose Self-Route, an approach where the model itself decides whether it needs retrieval or whether it can just go straight through context. The token savings are big and the quality loss is small.</p>
<p>Then <a href="https://openreview.net/forum?id=CLF25dahgA"target="_blank" rel="noopener"><strong>LaRA</strong></a>, presented at ICML 2025, is more measured. The authors built 2326 test cases across four QA task types and three long-context types, ran them across 11 different LLMs, and the conclusion was: there is no silver bullet. The choice between RAG and long context depends on the model, the context size, the task type, and the retrieval characteristics. RAG wins on dialogue and generic queries, long context wins on Wikipedia-style QA.</p>
<p><a href="https://arxiv.org/abs/2501.01880"target="_blank" rel="noopener"><strong>Long Context vs. RAG for LLMs: An Evaluation and Revisits</strong></a>, from January 2025, is the one that most reinforces this post&rsquo;s thesis. Long context tends to beat RAG on QA benchmarks, especially when the base document is stable. Summarization-based retrieval comes close, and chunk-based retrieval lags behind. In other words: the old way, chunk plus embed plus top-k, is the one that comes out worst.</p>
<p>Worth keeping on the radar too is the original <a href="https://arxiv.org/abs/2307.03172"target="_blank" rel="noopener"><strong>Lost in the Middle</strong></a> (Liu et al., 2023, published in TACL in 2024). That&rsquo;s the paper that showed even models with big windows have performance that depends on the position of the relevant information. Stuff at the beginning or end of the context is found easily; stuff in the middle degrades. For a long time this got used as the argument against long context, but the paper is from 2023, with 2023 models. Today&rsquo;s models, the Claude 4.x and Gemini 3.x line, handle the middle a lot better. It&rsquo;s not a solved problem, but it&rsquo;s much smaller than it was.</p>
<p>On the lexical retrieval side, <a href="https://arxiv.org/abs/2104.08663"target="_blank" rel="noopener"><strong>BEIR</strong></a> is still the canonical reference. The classic result is that BM25, all the way from the 90s, is still ridiculously competitive in out-of-domain scenarios. Dense models only win consistently when you have in-domain data to fine-tune the embeddings. In zero-shot scenarios, which is where most projects live, BM25 is hard to beat without serious work.</p>
<p>To wrap up, the <a href="https://www.anthropic.com/news/contextual-retrieval"target="_blank" rel="noopener"><strong>Anthropic post on Contextual Retrieval</strong></a>, from September 2024, is the most practical piece on the list. They show that combining contextual embedding with contextual BM25 drops the top-20 failure rate from 5.7% to 2.9%. Add a reranker and it drops to 1.9%. Important detail: BM25 is the centerpiece of their result, not a sidekick. The right reading is &ldquo;lexical plus vector plus reranker is the combination that works.&rdquo; Anyone who can only pick one picks BM25 and still gets pretty far.</p>
<p>To sum up what we can actually nail down: the literature isn&rsquo;t claiming &ldquo;RAG is dead.&rdquo; It&rsquo;s saying that long context, when you can use it, tends to win on quality. It&rsquo;s saying RAG&rsquo;s cost is still its main argument. It&rsquo;s saying lexical BM25 is much stronger than the vector DB marketing makes it sound. And it&rsquo;s saying that when you really do need heavy retrieval, the robust combination is hybrid (lexical plus vector plus reranker), not pure vector. All of that lines up with what I&rsquo;ve been defending in practice.</p>
<h2>Sources<span class="hx:absolute hx:-mt-20" id="sources"></span>
    <a href="#sources" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Li, Z. et al. (2024). <a href="https://arxiv.org/abs/2407.16833"target="_blank" rel="noopener">Retrieval Augmented Generation or Long-Context LLMs? A Comprehensive Study and Hybrid Approach</a>. EMNLP 2024 Industry Track.</li>
<li>Yuan, K. et al. (2025). <a href="https://openreview.net/forum?id=CLF25dahgA"target="_blank" rel="noopener">LaRA: Benchmarking Retrieval-Augmented Generation and Long-Context LLMs – No Silver Bullet for LC or RAG Routing</a>. ICML 2025.</li>
<li>Yu, T. et al. (2025). <a href="https://arxiv.org/abs/2501.01880"target="_blank" rel="noopener">Long Context vs. RAG for LLMs: An Evaluation and Revisits</a>. arXiv:2501.01880.</li>
<li>Liu, N. F. et al. (2023). <a href="https://arxiv.org/abs/2307.03172"target="_blank" rel="noopener">Lost in the Middle: How Language Models Use Long Contexts</a>. TACL 2024.</li>
<li>Thakur, N. et al. (2021). <a href="https://arxiv.org/abs/2104.08663"target="_blank" rel="noopener">BEIR: A Heterogenous Benchmark for Zero-shot Evaluation of Information Retrieval Models</a>. NeurIPS Datasets and Benchmarks 2021.</li>
<li>Anthropic (2024). <a href="https://www.anthropic.com/news/contextual-retrieval"target="_blank" rel="noopener">Introducing Contextual Retrieval</a>. Blog post.</li>
<li>Akita, F. (2026). <a href="/en/2026/03/31/claude-code-source-code-leaked-what-we-found-inside/">Claude Code&rsquo;s Source Code Leaked. Here&rsquo;s What We Found Inside.</a> — my coverage of the leak, with more detail on the memory architecture, KAIROS and <code>autoDream</code>.</li>
</ul>
]]></content:encoded><category>llms</category><category>software-architecture</category><category>databases</category></item><item><title>Sharing models between Rails Apps - Part 2</title><link>https://akitaonrails.github.io/en/2016/10/03/sharing-models-between-rails-apps-part-2/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/10/03/sharing-models-between-rails-apps-part-2/</guid><pubDate>Mon, 03 Oct 2016 15:57:00 -0300</pubDate><description>&lt;p&gt;Let&amp;rsquo;s continue from where I left off in &lt;a href="http://www.akitaonrails.com/en/2016/10/03/compartilhando-models-entre-aplicacoes-rails-parte-1"target="_blank" rel="noopener"&gt;Part 1&lt;/a&gt; where I quickly described how you can extract reusable model logic from a Rails app into a testable Rubygem.&lt;/p&gt;
&lt;p&gt;If I were building a secondary Rails app connecting directly to the same database as the first, I could just add the dependency to the extracted gem:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;central-support&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Codeminer42/cm42-central-support&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;master&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;central/support&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Then recreate the models including the same Concerns, and make sure I remove the ability to &lt;code&gt;bin/rails db:migrate&lt;/code&gt; from the secondary app (by creating empty db tasks with the same name, for example).&lt;/p&gt;</description><content:encoded><![CDATA[<p>Let&rsquo;s continue from where I left off in <a href="http://www.akitaonrails.com/en/2016/10/03/compartilhando-models-entre-aplicacoes-rails-parte-1"target="_blank" rel="noopener">Part 1</a> where I quickly described how you can extract reusable model logic from a Rails app into a testable Rubygem.</p>
<p>If I were building a secondary Rails app connecting directly to the same database as the first, I could just add the dependency to the extracted gem:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;central-support&#39;</span><span class="p">,</span> <span class="ss">github</span><span class="p">:</span> <span class="s1">&#39;Codeminer42/cm42-central-support&#39;</span><span class="p">,</span> <span class="ss">branch</span><span class="p">:</span> <span class="s1">&#39;master&#39;</span><span class="p">,</span> <span class="nb">require</span><span class="p">:</span> <span class="s1">&#39;central/support&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then recreate the models including the same Concerns, and make sure I remove the ability to <code>bin/rails db:migrate</code> from the secondary app (by creating empty db tasks with the same name, for example).</p>
<p>By the way, this is one big caveat that I didn&rsquo;t address in Part 1: up to this point, the schema was <strong>frozen</strong> in the <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/support/rails_app/db/schema.rb"target="_blank" rel="noopener">central-support gem</a>.</p>
<p>From now on, you must control the evolution of the tables mapped in the gem from within the gem. The best approach is to use the <code>spec/support/rails_app</code> and normally create new migrations with <code>bin/rails g migration</code> from there. Then you must move the migration to the <code>lib/generators/central/templates/migrations</code> folder.</p>
<p>The <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/lib/generators/central/install_generator.rb"target="_blank" rel="noopener"><code>lib/generators/central/install_generator.rb</code></a> will take care of making a <code>central:install</code> task available that will dutifully put new migrations into your application&rsquo;s <code>db/migrate</code> folder as usual. You just have to <code>bundle update central-support</code> to get the newest changes, run <code>bin/rails g central:install</code> to create the new migrations (it will automatically skip existing ones) and run the normal <code>bin/rails db:migrate</code>. A migration generator code is very simple, you can do it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;securerandom&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails/generators&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails/generators/base&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails/generators/active_record&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Central</span>
</span></span><span class="line"><span class="cl">  <span class="k">module</span> <span class="nn">Generators</span>
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="nc">InstallGenerator</span> <span class="o">&lt;</span> <span class="no">Rails</span><span class="o">::</span><span class="no">Generators</span><span class="o">::</span><span class="no">Base</span>
</span></span><span class="line"><span class="cl">      <span class="kp">include</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Generators</span><span class="o">::</span><span class="no">Migration</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">source_root</span> <span class="no">File</span><span class="o">.</span><span class="n">expand_path</span><span class="p">(</span><span class="s2">&#34;../templates&#34;</span><span class="p">,</span> <span class="bp">__FILE__</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">def</span> <span class="nf">create_migrations</span>
</span></span><span class="line"><span class="cl">        <span class="no">Dir</span><span class="o">[</span><span class="s2">&#34;</span><span class="si">#{</span><span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">source_root</span><span class="si">}</span><span class="s2">/migrations/*.rb&#34;</span><span class="o">].</span><span class="n">sort</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">filepath</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">          <span class="nb">name</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="n">filepath</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">migration_template</span> <span class="s2">&#34;migrations/</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;db/migrate/</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="ss">skip</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">          <span class="nb">sleep</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <code>migration_template</code> will take care of adding the proper timestamp to the migration file, so you don&rsquo;t have to add it manually and the template file name can be something plain such as <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/lib/generators/central/templates/migrations/add_role_field_to_users.rb"target="_blank" rel="noopener"><code>migrations/add_role_field_to_user.rb</code></a>.</p>
<p>All that having been said, there is a second challenge I added to my internal secondary app: I wanted it to have its own main database and use Central&rsquo;s database as a secondary read-only source.</p>
<p>So migrations in the secondary app (let&rsquo;s just call it Central-2) will run against its own main database, not against the main Central&rsquo;s database. This adds the following problem: the test suite must be able to create and migrate both test databases, in isolation from Central. Only in production should Central-2 connect to Central&rsquo;s database.</p>
<p>Every Rails application has a <code>config/database.yml.sample</code> and a <code>db/schema.rb</code>, so I started by creating a <code>config/database_central.yml.sample</code> and a <code>db_central/schema.rb</code>.</p>
<p>The <code>config/database_central.yml.sample</code> is already interesting:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">development</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">adapter</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">encoding</span><span class="p">:</span><span class="w"> </span><span class="l">unicode</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="l">fulcrum_development</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pool</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Warning: The database defined as &#34;test&#34; will be erased and</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># re-generated from your development database when you run &#34;rake&#34;.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Do not set this db to the same as development or production.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">adapter</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">encoding</span><span class="p">:</span><span class="w"> </span><span class="l">unicode</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">&lt;% if ENV[&#39;DATABASE_CENTRAL_URL&#39;] %&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;%= ENV[&#39;DATABASE_CENTRAL_URL&#39;] %&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">&lt;% else %&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="l">central_test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">&lt;% end %&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pool</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">production</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">adapter</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">encoding</span><span class="p">:</span><span class="w"> </span><span class="l">unicode</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;%= ENV[&#39;DATABASE_CENTRAL_URL&#39;] %&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pool</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;%= ENV[&#39;DB_POOL&#39;] || 5 %&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In production, it will use the <code>DATABASE_CENTRAL_URL</code> environment variable to connect to Central&rsquo;s main database.</p>
<p>When running tests locally, it will simply connect to a local database named <code>central_test</code>.</p>
<p>Now, while running tests at Gitlab-CI (or any other CI for that matter), I have to configure the <code>DATABASE_CENTRAL_URL</code> to point to a secondary Postgresql test database.</p>
<p>For Gitlab, this is how I configure the build script:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">codeminer42/ci-ruby:2.3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">postgres:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">cache</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">central-bot</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">untracked</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">.ci_cache/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">RAILS_ENV</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">DATABASE_URL</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql://postgres:@postgres</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">DATABASE_CENTRAL_URL</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql://postgres:@postgres/central_test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">before_script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">bundle install --without development production -j $(nproc) --path .ci_cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">cp config/database.yml.sample config/database.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">cp config/database_central.yml.sample config/database_central.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">bin/rails --trace central:db:create central:db:schema:load</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">bin/rails --trace db:create db:schema:load</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">bundle exec rspec</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notice how I copy the &ldquo;.sample&rdquo; config files to make sure they exist. And then how I run tasks you know such as <code>db:create db:schema:load</code> to create the normal test database, but also tasks you don&rsquo;t know such as <code>central:db:create central:db:schema:load</code>.</p>
<p>I defined those tasks in <code>lib/tasks/db_central.rake</code> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">task</span> <span class="ss">spec</span><span class="p">:</span> <span class="o">[</span><span class="s2">&#34;central:db:test:prepare&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">namespace</span> <span class="ss">:central</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">namespace</span> <span class="ss">:db</span> <span class="k">do</span> <span class="o">|</span><span class="n">ns</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="s2">&#34;environment:set&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:environment:set&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="ss">:drop</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:drop&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="ss">:create</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:create&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="ss">:setup</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:setup&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="ss">:reset</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:reset&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="ss">:migrate</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:migrate&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="ss">:rollback</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:rollback&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="ss">:seed</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:seed&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="ss">:version</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:version&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">namespace</span> <span class="ss">:schema</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">task</span> <span class="ss">:load</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:schema:load&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">task</span> <span class="ss">:dump</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:schema:dump&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">namespace</span> <span class="ss">:test</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">task</span> <span class="ss">:prepare</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:test:prepare&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># append and prepend proper tasks to all the tasks defined here above</span>
</span></span><span class="line"><span class="cl">    <span class="n">ns</span><span class="o">.</span><span class="n">tasks</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">task</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">task</span><span class="o">.</span><span class="n">enhance</span> <span class="o">[</span><span class="s2">&#34;central:set_custom_config&#34;</span><span class="o">]</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;central:revert_to_original_config&#34;</span><span class="o">].</span><span class="n">invoke</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">task</span> <span class="ss">:set_custom_config</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># save current vars</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@original_config</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="ss">env_schema</span><span class="p">:</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;SCHEMA&#39;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">config</span><span class="p">:</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">dup</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># set config variables for custom database</span>
</span></span><span class="line"><span class="cl">    <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;SCHEMA&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;db_central/schema.rb&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">paths</span><span class="o">[</span><span class="s1">&#39;db&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;db_central&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">paths</span><span class="o">[</span><span class="s1">&#39;db/migrate&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;db_central/migrate&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">paths</span><span class="o">[</span><span class="s1">&#39;db/seeds&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;db_central/seeds.rb&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">paths</span><span class="o">[</span><span class="s1">&#39;config/database&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;config/database_central.yml&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Tasks</span><span class="o">::</span><span class="no">DatabaseTasks</span><span class="o">.</span><span class="n">database_configuration</span> <span class="o">=</span> <span class="no">CM</span><span class="p">(</span><span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;config&#34;</span><span class="p">,</span> <span class="s2">&#34;database_central.yml&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">File</span><span class="o">.</span><span class="n">read</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">ERB</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">result</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">YAML</span><span class="o">.</span><span class="n">load</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">unwrap</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">task</span> <span class="ss">:revert_to_original_config</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># reset config variables to original values</span>
</span></span><span class="line"><span class="cl">    <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;SCHEMA&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="vi">@original_config</span><span class="o">[</span><span class="ss">:env_schema</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span> <span class="o">=</span> <span class="vi">@original_config</span><span class="o">[</span><span class="ss">:config</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is how I define a namespace of Rake tasks that start with <code>central:</code>, and every one of those connects to the secondary database as described in <code>database_central.yml</code>. The weird syntax is from my <a href="https://github.com/akitaonrails/chainable_methods"target="_blank" rel="noopener"><code>chainable_methods</code></a>, don&rsquo;t mind it too much.</p>
<p>The <code>db_central/schema.rb</code> is basically a copy of the <code>spec/support/rails_app/db/schema.rb</code> from the central-support gem, with the same tables. The spec runner of both the gem and this secondary app will just load the schema into the test database.</p>
<p>Now that we have the basic underpinnings for specs in place, we can focus on how the application itself can consume those external models.</p>
<p>We start by adding an initializer like <code>config/initializer/db_central.rb</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">DB_CENTRAL</span> <span class="o">=</span> <span class="no">CM</span><span class="p">(</span><span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;config&#34;</span><span class="p">,</span> <span class="s2">&#34;database_central.yml.sample&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">File</span><span class="o">.</span><span class="n">read</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">ERB</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">result</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">YAML</span><span class="o">.</span><span class="n">load</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">unwrap</span><span class="o">.</span><span class="n">freeze</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In this case I am reading from the sample file because, unlike the CI build, when I deploy to Heroku I don&rsquo;t have a script to copy the sample to the final yaml file. This will populate the constant <code>DB_CENTRAL</code> with the database URL stored in the <code>DATABASE_CENTRAL_URL</code> environment variable that I have to set.</p>
<p>Then I create a new file called <code>app/models/remote_application_record.rb</code> that looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">RemoteApplicationRecord</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
</span></span><span class="line"><span class="cl">  <span class="n">establish_connection</span> <span class="no">DB_CENTRAL</span><span class="o">[</span><span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="nb">self</span><span class="o">.</span><span class="n">abstract_class</span> <span class="o">=</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">unless</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">test?</span>
</span></span><span class="line"><span class="cl">    <span class="n">default_scope</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">readonly</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is how you create a new connection pool for the secondary database configuration. You must only have this <code>establish_connection</code> in one place and have the models inherit from here. The <code>abstract_class = true</code> will make ActiveRecord not try to load from a table of the same name as this class.</p>
<p>Then we have a <code>default_scope</code> locking down the model as <code>readonly</code>. We don&rsquo;t want that in the test environment because I still want to have Factory Girl populate the test database with fake data for the specs. But it&rsquo;s a good idea to have it in the production environment just to make sure.</p>
<p>Finally, I can create all the models I need, such as <code>app/models/central/team.rb</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Central</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">Team</span> <span class="o">&lt;</span> <span class="no">RemoteApplicationRecord</span>
</span></span><span class="line"><span class="cl">    <span class="nb">self</span><span class="o">.</span><span class="n">table_name</span> <span class="o">=</span> <span class="s1">&#39;teams&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Associations</span>
</span></span><span class="line"><span class="cl">    <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Validations</span>
</span></span><span class="line"><span class="cl">    <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Scopes</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>From here I can just call normal Arel queries such as <code>Central::Team.not_archived.limit(5)</code>.</p>
<h3>Conclusions<span class="hx:absolute hx:-mt-20" id="conclusions"></span>
    <a href="#conclusions" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you didn&rsquo;t already, refer to <a href="http://www.akitaonrails.com/en/2016/10/03/compartilhando-models-entre-aplicacoes-rails-parte-1"target="_blank" rel="noopener">Part 1</a> for more details.</p>
<p>This is a simple recipe to share model logic between a main read-write Rails app and a secondary read-only Rails app. They share most (not all) of the same models, they share the same logic (through some of the Concerns), and they share the same database.</p>
<p>In this particular case, the recommended approach is to create a <a href="https://devcenter.heroku.com/articles/heroku-postgres-follower-databases"target="_blank" rel="noopener">Follower database</a>, which is how Heroku calls a secondary replicated database, and make my secondary application connect to that (since it only needs a read-only source).</p>
<p>For more complicated scenarios, you will need a more complicated solution such as an HTTP API layer to make sure only one App manages model migrations and such. But the Rubygem approach should be &ldquo;good enough&rdquo; for many cases.</p>
<p>If I really need to go that way, it won&rsquo;t be too difficult to transform this small gem into a full-blown Rails API app. If you can&rsquo;t even separate the logic as a Concern, you won&rsquo;t be able to separate them as APIs either, so consider this a quick exercise, a first step towards creating an <a href="http://programmers.stackexchange.com/questions/184464/what-is-an-anti-corruption-layer-and-how-is-it-used"target="_blank" rel="noopener">Anti Corruption Layer</a>.</p>
<p>And as a bonus, consider contributing to the <a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener">Central</a> and <a href="https://github.com/Codeminer42/cm42-central-support"target="_blank" rel="noopener">central-support</a> open source projects. I intend to build a competitive Pivotal Tracker/Trello alternative and we are getting there!</p>
]]></content:encoded><category>rails</category><category>databases</category><category>software-architecture</category></item><item><title>Hacking Mattermost Team Edition</title><link>https://akitaonrails.github.io/en/2016/08/12/hacking-mattermost-team-edition/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/08/12/hacking-mattermost-team-edition/</guid><pubDate>Fri, 12 Aug 2016 14:43:00 -0300</pubDate><description>&lt;p&gt;In &lt;a href="http://www.akitaonrails.com/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener"&gt;my previous post&lt;/a&gt; I was reporting on my move from Slack to Rocket.chat. But I also mentioned that before Rocket.chat I would rather use Mattermost. First and foremost because it&amp;rsquo;s written in Go (lightweight, highly concurrent, super stable), and because the code base shows much more quality than Rocket.chat (which feels super fragile, with almost no automated tests at all).&lt;/p&gt;
&lt;p&gt;But my major complaint with Mattermost is that the free, open source, Team Edition lacks a super important feature: not allowing users to delete private groups.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In <a href="http://www.akitaonrails.com/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener">my previous post</a> I was reporting on my move from Slack to Rocket.chat. But I also mentioned that before Rocket.chat I would rather use Mattermost. First and foremost because it&rsquo;s written in Go (lightweight, highly concurrent, super stable), and because the code base shows much more quality than Rocket.chat (which feels super fragile, with almost no automated tests at all).</p>
<p>But my major complaint with Mattermost is that the free, open source, Team Edition lacks a super important feature: not allowing users to delete private groups.</p>
<p><a href="http://www.akitaonrails.com/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough#comment-2832915684"target="_blank" rel="noopener">@iantien commented</a> that the private groups are never actually &ldquo;deleted&rdquo;, they are just marked as deleted, audited, but all data is still in the database. Just the UI has no way to hide the &ldquo;delete&rdquo; option from users and there is no Administration UI to unarchive the deleted groups.</p>
<p>In fact, you can open a <code>psql</code> session in your PostgreSQL database and just do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>update channels set deleteat = 0;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This will unarchive and restore all deleted channels. But you can see how this is a hassle.</p>
<p>I strongly disagree when he says that only &ldquo;10,000-user enterprises&rdquo; would need such a feature. Even in a small team, any grumpy user can just archive a channel out of the blue and disrupt the entire team communication in private groups. Sure, the &ldquo;Town Square&rdquo; and other public channels will still function, but if you have just one external user participating in projects, for example, you want to use private groups to isolate your internal communication from external users.</p>
<p>So, not having the option for very basic permissions (such as disallowing members to delete channels or private groups) is a very big show stopper even for small teams. And sure, the USD 20/user/year fee is not expensive, but as Mattermost still has fewer features than Rocket.chat, it becomes a very hard sell.</p>
<p>Also, hacking the code itself and adding a flag to disallow this option, in Go, is actually quite easy, but you would have to maintain your own fork (as I think Mattermost would not accept a pull request for a feature that already is in their paid, Enterprise offering).</p>
<p>But after @iantien commented that nothing is deleted and it&rsquo;s all audited, I quickly realized that I could use the audit metadata and devise a way to automatically restore the channels (unless it&rsquo;s the system admin doing it). All without altering the source code.</p>
<p>One can use the many tools available in PostgreSQL itself, namely: <strong>TRIGGERS</strong>. So, without further ado, just run this in your Mattermost database:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="k">REPLACE</span><span class="w"> </span><span class="k">FUNCTION</span><span class="w"> </span><span class="n">undelete_channel</span><span class="p">()</span><span class="w"> </span><span class="k">RETURNS</span><span class="w"> </span><span class="k">trigger</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="err">$$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">DECLARE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">user_counter</span><span class="w"> </span><span class="nb">integer</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">channel_id</span><span class="w"> </span><span class="nb">character</span><span class="w"> </span><span class="nb">varying</span><span class="p">(</span><span class="mi">26</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">BEGIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">-- Only for channel delete operations
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">IF</span><span class="w"> </span><span class="k">NEW</span><span class="p">.</span><span class="n">action</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;%/channels/%/delete&#39;</span><span class="w"> </span><span class="k">THEN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">RETURN</span><span class="w"> </span><span class="k">NEW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">END</span><span class="w"> </span><span class="k">IF</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">-- Check if it is the system_admin
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">user_counter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">NEW</span><span class="p">.</span><span class="n">userid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">AND</span><span class="w"> </span><span class="n">roles</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;system_admin&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">IF</span><span class="w"> </span><span class="n">user_counter</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k">THEN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">RETURN</span><span class="w"> </span><span class="k">NEW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">END</span><span class="w"> </span><span class="k">IF</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">channel_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">split_part</span><span class="p">(</span><span class="k">NEW</span><span class="p">.</span><span class="n">action</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;/&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">7</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">UPDATE</span><span class="w"> </span><span class="n">channels</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">SET</span><span class="w"> </span><span class="n">deleteat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">channel_id</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">RETURN</span><span class="w"> </span><span class="k">NEW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">END</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="err">$$</span><span class="w"> </span><span class="k">LANGUAGE</span><span class="w"> </span><span class="n">plpgsql</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">DROP</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">undelete_channel</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">audits</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="n">undelete_channel</span><span class="w"> </span><span class="k">AFTER</span><span class="w"> </span><span class="k">INSERT</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">audits</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">FOR</span><span class="w"> </span><span class="k">EACH</span><span class="w"> </span><span class="k">ROW</span><span class="w"> </span><span class="k">EXECUTE</span><span class="w"> </span><span class="k">PROCEDURE</span><span class="w"> </span><span class="n">undelete_channel</span><span class="p">();</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That&rsquo;s it, it will listen for new audit inserts, check if it is a &ldquo;channel delete&rdquo; action, check that it is not a &lsquo;system_admin&rsquo;, and if so it will automatically grab the channel id from the action REST URL and do the proper UPDATE to bring it back.</p>
<p>I tested it already and in my UI users don&rsquo;t even realize something happened. Not even the offending user sees the channel go away, it instantly comes back.</p>
<p>So, if this was the only thing stopping you from using the free-of-charge, on-premise Team Edition, there you go. And with this you can derive functions to also prevent channels from being renamed, but I&rsquo;ll leave it as an exercise for you (please share in the comments section below if you do it).</p>
<p>Happy Hacking!</p>
]]></content:encoded><category>databases</category><category>automation</category></item><item><title>Moving away from Slack into Rocket.chat. Good Enough.</title><link>https://akitaonrails.github.io/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough/</guid><pubDate>Tue, 09 Aug 2016 16:21:00 -0300</pubDate><description>&lt;p&gt;In my quest to create my own private infrastructure I&amp;rsquo;ve already moved away from Pivotal Tracker (saving this for a future post), Bitbucket, Github, Travis and into GitLab and GitLab Runner. There are more services I will move away from in the future, so expect more posts like this.&lt;/p&gt;
&lt;p&gt;In &lt;a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener"&gt;my previous post&lt;/a&gt; I forgot to mention minimal firewall and SSL configuration, but you can check it here.&lt;/p&gt;
&lt;p&gt;One of the most difficult pieces to move away from is Slack. Don&amp;rsquo;t get me wrong, it&amp;rsquo;s a great tool and it gets the job done. The free-tier is really competent but I really want some of the paid features such as history of all conversations with search capabilities and uploads without limit.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In my quest to create my own private infrastructure I&rsquo;ve already moved away from Pivotal Tracker (saving this for a future post), Bitbucket, Github, Travis and into GitLab and GitLab Runner. There are more services I will move away from in the future, so expect more posts like this.</p>
<p>In <a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener">my previous post</a> I forgot to mention minimal firewall and SSL configuration, but you can check it here.</p>
<p>One of the most difficult pieces to move away from is Slack. Don&rsquo;t get me wrong, it&rsquo;s a great tool and it gets the job done. The free-tier is really competent but I really want some of the paid features such as history of all conversations with search capabilities and uploads without limit.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/555/big_Franz.png" alt="Codeminer Rocket"  loading="lazy" /></p>
<h2>The Cost Conundrum<span class="hx:absolute hx:-mt-20" id="the-cost-conundrum"></span>
    <a href="#the-cost-conundrum" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>But then you have to commit to pay USD 8/month per user (USD 6.67 if you pay ahead of time). If you&rsquo;re a small team (around a dozen people) it&rsquo;s well worth it and you should definitely pay and be done with it.</p>
<p>If you have more than 20 people then it starts to become a conundrum. On my GitLab cost calculations, some people argued that saving USD 15k yearly is not such a big deal, and they&rsquo;re not wrong.</p>
<p>If you&rsquo;re from outside of the US coastal cities, especially from countries in South America or Asia where currency can be more than 3 times weaker, then it quickly starts to become a big deal.</p>
<p>Apart from that, we are currently offloading a whole lot of our IPs and knowledge into the care of companies we don&rsquo;t really know and where we don&rsquo;t hold any real stakes.</p>
<p>If you&rsquo;re a medium to big company it really starts to make sense to want to have more control over your own property. Of course, it costs some work and maintenance, so it&rsquo;s not for everybody.</p>
<p>What I will argue is that if you have <strong>150 people</strong> or more, and you&rsquo;re still growing, it&rsquo;s not such a big burden to jump into your own infrastructure.</p>
<blockquote>
  <p>You avoid paying around <a href="https://codeminer42.slack.com/pricing"target="_blank" rel="noopener"><strong>USD 12,000 a year</strong></a> or more, and instead you will pay <strong>USD 960 a year</strong> in Digital Ocean boxes and MMS plus a few hours every month in basic maintenance. That&rsquo;s 12 times cheaper.</p>

</blockquote>
<p>Again, you shouldn&rsquo;t do it just for the cost savings as the maintenance burden can easily outweigh the savings if you or your team are not devops savvy and you don&rsquo;t have anyone to keep monitoring it. For most companies, the conversation history is not so important and even the free-tier will be more than enough.</p>
<p>Cost is less relevant depending on your context, and there are many different uses for group chats. For example, if you&rsquo;re an <a href="https://news.ycombinator.com/item?id=9754626"target="_blank" rel="noopener">open source community</a>, you would try <a href="https://gitter.im"target="_blank" rel="noopener">Gitter</a> instead.</p>
<p>There are also <a href="https://www.google.com/search?q=slack&#43;alternatives"target="_blank" rel="noopener">many other alternatives</a> that you may want to try.</p>
<p>For the sake of this article, let&rsquo;s assume you&rsquo;re a small to medium company, in need of private rooms, many external users (for example, your clients), with at least 50 people. Then this can start to make sense. Any other scenario and we have to analyze it differently.</p>
<h2>Why I chose Rocket.chat<span class="hx:absolute hx:-mt-20" id="why-i-chose-rocketchat"></span>
    <a href="#why-i-chose-rocketchat" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>With the cost reasoning out of the way, then it&rsquo;s a matter of choosing which alternative to use.</p>
<p>I tried <a href="https://www.mattermost.org/"target="_blank" rel="noopener">MatterMost</a> first and I really wanted to like it. It has a very similar feature set to Slack and most importantly, it&rsquo;s written in Go. So it&rsquo;s both lightweight, very fast, and not so difficult to contribute. It uses MySQL or PostgreSQL which I like.</p>
<p>Unfortunately, it has an important <strong>show-stopper</strong> for my needs. You can create both public channels or private groups, but anyone can delete a private group. If you want permission control for that you must use their <a href="https://about.mattermost.com/pricing/"target="_blank" rel="noopener">&ldquo;Enterprise&rdquo; offering</a> and pay <strong>USD 20/year per user</strong>.</p>
<blockquote>
  <p>It&rsquo;s still at least <strong>3 times cheaper</strong> than Slack, so it may be a good option for you.</p>

</blockquote>
<p>I really want to have full-control, not just make it cheaper. I could pay Slack already if I didn&rsquo;t care, so I would not just pay an alternative and not have full control (including the ability to tweak and improve the entire code base).</p>
<p>Then, it took me back to <a href="https://rocket.chat/"target="_blank" rel="noopener">Rocket.chat</a>. Its feature set also rivals Slack, it&rsquo;s good looking enough. But, it&rsquo;s made in Meteor. Now, I don&rsquo;t have anything against Meteor and I really think a Slack-clone is exactly the kind of use case where you could use Meteor to its full potential.</p>
<p>Technically I really think it&rsquo;s a downside to be forced to use MongoDB (Meteor requires Mongo). For small installations I really prefer to have PostgreSQL. MongoDB is competent, but for medium to big installations there are caveats, and I will show you why below.</p>
<p>I <strong>strongly dislike</strong> the <a href="https://medium.com/friendship-dot-js/i-peeked-into-my-node-modules-directory-and-you-wont-believe-what-happened-next-b89f63d21558#.8jd3z3n6u"target="_blank" rel="noopener">culture of writing software without <em>minimal care</em></a> such as having a reasonably complete test suite. You browse through the many packages that comprise Rocket.chat and several of them have no tests whatsoever. Some packages do have jasmine tests, but the majority is lacking.</p>
<p>This is <a href="https://github.com/RocketChat/Rocket.Chat/blob/a5cb22bb0017f4c39654bf1e2895ae64acb0339b/packages/rocketchat-katex/tests/jasmine/client/unit/katex.spec.coffee"target="_blank" rel="noopener">one example</a> I picked randomly, out of the few test files I found:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">describe</span> <span class="s1">&#39;rocketchat:katex Client&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">it</span> <span class="s1">&#39;should exist&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">RocketChat</span><span class="p">.</span><span class="nx">katex</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is sad, sorry to say that, but it is. Not surprisingly I&rsquo;ve seen katex issues while using the interface. And if you think this was just a poor choice, I picked <a href="https://github.com/RocketChat/Rocket.Chat/blob/a5cb22bb0017f4c39654bf1e2895ae64acb0339b/packages/rocketchat-markdown/tests/jasmine/client/unit/markdown.spec.coffee"target="_blank" rel="noopener">another test file</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">describe</span> <span class="s1">&#39;rocketchat:markdown Client&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">it</span> <span class="s1">&#39;should exist&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">RocketChat</span><span class="p">.</span><span class="nx">Markdown</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Really?</p>
<p>So, the code itself is not so pretty, you have to keep this in mind. I&rsquo;d much rather use Mattermost (which is much better structured and with enough tests), but it&rsquo;s not entirely free, so it&rsquo;s not an option for me. For my scenario, I&rsquo;d rather have sloppy code (that minimally &ldquo;works&rdquo;) that I can tweak than code I can&rsquo;t see, for this particular venture. Many inexperienced developers may argue about the necessity of having automated tests, but not having them also impacts this:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/552/big_Contributors_to_RocketChat_Rocket_Chat.png" alt="Rocket.chat contribution graph"  loading="lazy" /></p>
<p>If we see how the project is evolving over time, we can see that interest is diminishing. The lack of good automated tests also makes it more difficult for new people to contribute without adding regressions, and makes contribution reviews more difficult and time consuming.</p>
<p>On the other hand the feature set is good enough as it is. There is only so much you can add to a Slack-clone. The upside is that if the code doesn&rsquo;t change too much, I can tweak it more without having many things breaking all the time. It&rsquo;s the complete opposite from the impressive contribution boost over at GitLab, for example.</p>
<p>Doing a few days of testing through the administration settings, user interface, having dozens of users doing real conversation, convinced me that even though the code quality is bad, the feature set is indeed there. I think I can live with that. Your mileage may vary though.</p>
<p>I am using <a href="http://meetfranz.com"target="_blank" rel="noopener">Franz</a> to open Slack, Rocket.chat, Hangouts, Messenger and any number of other communicators in one single app, which is great. And Rocket.chat does provide <a href="https://itunes.apple.com/us/app/rocket.chat/id1028869439?mt=8"target="_blank" rel="noopener">mobile</a> <a href="https://play.google.com/store/apps/details?id=com.konecty.rocket.chat&amp;hl=en"target="_blank" rel="noopener">apps</a> based off PhoneGap. Again, it&rsquo;s good, not great.</p>
<p>Many people have heavy bots or integrations into their Slack configuration. The alternatives such as Rocket.chat do support incoming and outgoing webhooks and most of the integrations should be possible (with some tweaking and hacking such as this <a href="https://github.com/FinndropStudios/GifRocket"target="_blank" rel="noopener">Giphy example</a>), so make a list of all important integrations and do some Google research on that before continuing.</p>
<h2>Planning the Infrastructure<span class="hx:absolute hx:-mt-20" id="planning-the-infrastructure"></span>
    <a href="#planning-the-infrastructure" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>All that having been said, I&rsquo;d recommend you try Rocket.chat&rsquo;s <a href="https://demo.rocket.chat"target="_blank" rel="noopener">demo server</a> first to see if it has the minimal features you need. And also that you install a dry-run in a small Digital Ocean box with the intention of blowing it away later.</p>
<p>For a single-instance, stand-alone installation, temporary dry-run, I recommend you start by installing the <a href="https://www.digitalocean.com/community/tutorials/how-to-use-the-mongodb-one-click-application"target="_blank" rel="noopener">MongoDB One-Click Application</a> and follow <a href="https://www.digitalocean.com/community/tutorials/how-to-install-configure-and-deploy-rocket-chat-on-ubuntu-14-04"target="_blank" rel="noopener">this instruction</a> to install everything in one machine.</p>
<p>This is the worst possible production environment, which is why I am being so repetitive in saying that you should destroy this box after you do your testing.</p>
<p>As I&rsquo;ve said before, I am assuming a scenario where you have 50 or more people, with real clients, important communication going on. You don&rsquo;t want to keep a simple installation like this for production usage.</p>
<p>For a <strong>minimal production installation</strong> you will need at the very least 4 boxes with 1 GB of RAM each. Now, why 4?</p>
<p>This is an important section for most of the beginner Javascript programmers out there playing with the so called &ldquo;MEAN&rdquo; stack (MongoDB, Express, Angularjs, Node.js). I&rsquo;ve seen people deploy single-instance, stand-alone &ldquo;MEAN&rdquo; apps for production usage. And this is super bad.</p>
<blockquote>
  <p><strong>MongoDB is NOT MEANT to function with just one single instance!</strong></p>

</blockquote>
<p>More than that:</p>
<blockquote>
  <p><strong>DO NOT start an even number of MongoDB nodes, or at least install an Arbiter</strong></p>

</blockquote>
<p>So, the minimal setup for MongoDB is a <a href="https://docs.mongodb.com/manual/core/replica-set-architecture-three-members/"target="_blank" rel="noopener">Three-member Replica Set</a>, no less!</p>
<p>The article <a href="https://www.rainforestqa.com/blog/2012-11-05-mongodb-gotchas-and-how-to-avoid-them/"target="_blank" rel="noopener">&ldquo;MongoDB Gotchas &amp; How To Avoid Them&rdquo;</a> is a little bit old but you should be aware of several of the documented gotchas, including adding replica sets and avoiding waiting too long to start sharding your data.</p>
<p>The most important part of making Rocket.chat reliable is to make your MongoDB minimally reliable, and this is a hidden cost you must consider.</p>
<h4>Installing a Three-Member Replica Set<span class="hx:absolute hx:-mt-20" id="installing-a-three-member-replica-set"></span>
    <a href="#installing-a-three-member-replica-set" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>For the sake of this article I will assume that you created 3 (three) MongoDB One-Click Applications on Digital Ocean&rsquo;s, at the very least with 1 GB RAM boxes (2 GB RAM machines are a good choice if you&rsquo;re in doubt, but never the 512 Mb) and another bare Ubuntu 14.04 box to be the web server. Create all machines in the same sub-region, all of them with private networking and backup enabled.</p>
<p>Take note of the public IPs and private IPs, for the sake of this article let&rsquo;s say you have this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Public IP     Private IP    Hostname

192.160.10.1  10.0.0.1      mongo1.yourdomain.com
192.160.10.2  10.0.0.2      mongo2.yourdomain.com
192.160.10.3  10.0.0.3      mongo3.yourdomain.com
192.160.10.4  10.0.0.4      yourdomain.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In the commands and code snippets below, make sure to replace the fake IPs with your own private IPs.</p>
<p>As I said in <a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena?"target="_blank" rel="noopener">my GitLab post</a>, create a swap file, configure the locale, set it up for automatic security updates, and upgrade the Ubuntu packages.</p>
<p>The next first thing: configure the firewall in all machines (assuming <code>ufw</code> package is already installed):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo ufw reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow https
sudo ufw allow ssh
sudo ufw allow in on eth1 to any port 27017
sudo ufw enable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>We are allowing MongoDB&rsquo;s 27017 port only for machines in the same private networking (through eth1, the public IP goes through eth0). We&rsquo;re also allowing port 22 for SSH and although I am not listing it here in this article you should also consider installing and configuring <a href="https://www.digitalocean.com/community/tutorials/how-to-protect-ssh-with-fail2ban-on-ubuntu-14-04"target="_blank" rel="noopener">fail2ban</a>. Digital Ocean also has a <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-14-04"target="_blank" rel="noopener">good post on UFW</a> for more details.</p>
<p>Next thing to do on the MongoDB machines is to <a href="https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/"target="_blank" rel="noopener">Disable Transparent Huge Pages (THP)</a>. For that create a file <code>/etc/init.d/disable-transparent-hugepages</code>, with this content:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/sh
</span></span></span><span class="line"><span class="cl"><span class="c1">### BEGIN INIT INFO</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Provides:          disable-transparent-hugepages</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required-Start:    $local_fs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required-Stop:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># X-Start-Before:    mongod mongodb-mms-automation-agent</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Default-Start:     2 3 4 5</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Default-Stop:      0 1 6</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Short-Description: Disable Linux transparent huge pages</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Description:       Disable Linux transparent huge pages, to improve</span>
</span></span><span class="line"><span class="cl"><span class="c1">#                    database performance.</span>
</span></span><span class="line"><span class="cl"><span class="c1">### END INIT INFO</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="nv">$1</span> in
</span></span><span class="line"><span class="cl">  start<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -d /sys/kernel/mm/transparent_hugepage <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nv">thp_path</span><span class="o">=</span>/sys/kernel/mm/transparent_hugepage
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="o">[</span> -d /sys/kernel/mm/redhat_transparent_hugepage <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nv">thp_path</span><span class="o">=</span>/sys/kernel/mm/redhat_transparent_hugepage
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s1">&#39;never&#39;</span> &gt; <span class="si">${</span><span class="nv">thp_path</span><span class="si">}</span>/enabled
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s1">&#39;never&#39;</span> &gt; <span class="si">${</span><span class="nv">thp_path</span><span class="si">}</span>/defrag
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">unset</span> thp_path
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you can register it as a service:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo chmod 755 /etc/init.d/disable-transparent-hugepages
sudo update-rc.d disable-transparent-hugepages defaults
sudo /etc/init.d/disable-transparent-hugepages start</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Stop MongoDB in all 3 machines:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service mongod stop</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now comes a complicated procedure that you should not do wrong. It&rsquo;s only complicated because you have to do it in all 4 machines minding their correct private IP addresses. If you already have your own DNS server, you can skip this, otherwise follow through carefully.</p>
<p>You will have to edit both <code>/etc/hostname</code> and <code>/etc/hosts</code>. Each machine will have a particular name and they should be able to find the other machines by name. Let&rsquo;s start by editing the <code>/etc/hostname</code> on each machine. Use the table in the beginning of this section, for example, in the machine with public IP <code>192.160.10.1</code> you must edit its hostname to be <code>mongo1.yourdomain.com</code>, and so on for all the machines.</p>
<p>Once you have it done, you have to replace the line <code>127.0.0.1 localhost</code> in the <code>/etc/hosts</code> in each of them for the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>127.0.0.1 localhost mongo1.yourdomain.com
10.0.0.1 mongo1.yourdomain.com
10.0.0.2 mongo2.yourdomain.com
10.0.0.3 mongo3.yourdomain.com
10.0.0.4 yourdomain.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This snippet assumes you&rsquo;re in the machine with private IP <code>10.0.0.1</code>, of course. Make sure you change <code>mongo1.yourdomain.com</code> in the first line for the hostname of the machine you&rsquo;re in. If this is too difficult for you, you definitely should not try to maintain your own infrastructure, so beware.</p>
<p>Finally, let&rsquo;s edit the <code>/etc/mongod.conf</code> with these changes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>storage:
  dbPath: /mongodb-data
...
net:
  port: 27017
  bindIp: 0.0.0.0
...
replication:
  replSetName: rs0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should be exactly the same in all machines. And you must create this <code>/mongodb-data</code> directory like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo mkdir /mongodb-data
sudo chown -R mongodb:mongodb /mongodb-data</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now go ahead on machine <code>mongo1.yourdomain.com</code>, start up with <code>sudo service mongod start</code> and fire up the <code>mongo</code> command, and you should see something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># mongo
MongoDB shell version: 3.2.8
connecting to: test
&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Let&rsquo;s now configure the replica set by issuing this command in the MongoDB command-line interface:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rs.initiate()
rs.add(&#39;mongo2.yourdomain.com&#39;)
rs.add(&#39;mongo3.yourdomain.com&#39;)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And you can now log in to the secondary machines and <code>sudo service mongod start</code> in all of them. They should all start joining the replica set and syncing.</p>
<p>You can check the status of the replication like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&gt; rs.conf()

{
  &#34;set&#34; : &#34;rs0&#34;,
  &#34;date&#34; : ISODate(&#34;2016-08-09T17:33:10.466Z&#34;),
  &#34;myState&#34; : 1,
  &#34;term&#34; : NumberLong(-1),
  &#34;heartbeatIntervalMillis&#34; : NumberLong(2000),
  &#34;members&#34; : [
    {
      &#34;_id&#34; : 1,
      &#34;name&#34; : &#34;mongo1.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 1,
      &#34;stateStr&#34; : &#34;PRIMARY&#34;,
      &#34;uptime&#34; : 11727,
      &#34;optime&#34; : Timestamp(1470763990, 3),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:10Z&#34;),
      &#34;electionTime&#34; : Timestamp(1470752553, 1),
      &#34;electionDate&#34; : ISODate(&#34;2016-08-09T14:22:33Z&#34;),
      &#34;configVersion&#34; : 6,
      &#34;self&#34; : true
    },
    {
      &#34;_id&#34; : 2,
      &#34;name&#34; : &#34;mongo2.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 2,
      &#34;stateStr&#34; : &#34;SECONDARY&#34;,
      &#34;uptime&#34; : 11693,
      &#34;optime&#34; : Timestamp(1470763988, 4),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:08Z&#34;),
      &#34;lastHeartbeat&#34; : ISODate(&#34;2016-08-09T17:33:10.026Z&#34;),
      &#34;lastHeartbeatRecv&#34; : ISODate(&#34;2016-08-09T17:33:08.887Z&#34;),
      &#34;pingMs&#34; : NumberLong(0),
      &#34;syncingTo&#34; : &#34;mongo3.yourdomain.com:27017&#34;,
      &#34;configVersion&#34; : 6
    },
    {
      &#34;_id&#34; : 3,
      &#34;name&#34; : &#34;mongo3.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 2,
      &#34;stateStr&#34; : &#34;SECONDARY&#34;,
      &#34;uptime&#34; : 11693,
      &#34;optime&#34; : Timestamp(1470763988, 4),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:08Z&#34;),
      &#34;lastHeartbeat&#34; : ISODate(&#34;2016-08-09T17:33:09.994Z&#34;),
      &#34;lastHeartbeatRecv&#34; : ISODate(&#34;2016-08-09T17:33:08.655Z&#34;),
      &#34;pingMs&#34; : NumberLong(0),
      &#34;syncingTo&#34; : &#34;mongo1.yourdomain.com:27017&#34;,
      &#34;configVersion&#34; : 6
    }
  ],
  &#34;ok&#34; : 1
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Each box should be syncing data to another box, one of them will be marked &ldquo;PRIMARY&rdquo; and the others will be &ldquo;SECONDARY&rdquo;. If one of them &ldquo;dies&rdquo; for some reason, one of the others will be elected as PRIMARY until you add a replacement box. Read more details on this procedure in <a href="https://www.digitalocean.com/community/tutorials/how-to-implement-replication-sets-in-mongodb-on-an-ubuntu-vps"target="_blank" rel="noopener">Digital Ocean&rsquo;s post about implementing replication sets</a>.</p>
<h4>Adding your MongoDB environment to MMS<span class="hx:absolute hx:-mt-20" id="adding-your-mongodb-environment-to-mms"></span>
    <a href="#adding-your-mongodb-environment-to-mms" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Another thing beginners will not realize is that it&rsquo;s a good idea to monitor your services. At the very least you should install MongoDB Cloud Manager agents into your servers. Just set up an account at <a href="https://www.mongodb.com/cloud"target="_blank" rel="noopener">MongoDB Atlas</a>.</p>
<p>It&rsquo;s beyond the point of this article to explain Atlas, but you should have no trouble installing the agents with the Group ID and API Key provided. Then install the monitoring agent and have the Manager figure out your existing deployment and replica set.</p>
<p>The basic subscription for monitoring will cost you an extra USD 39 a month, but believe me when I say it&rsquo;s worth it to add all your MongoDB deployments under MMS.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/553/big_Deployment___Cloud_Manager__MongoDB_Cloud_Manager.png" alt="MMS"  loading="lazy" /></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/554/big_Host___Cloud_Manager__MongoDB_Cloud_Manager.png" alt="Replica Set Graphs"  loading="lazy" /></p>
<h4>Installing a multi-instance Node.js service<span class="hx:absolute hx:-mt-20" id="installing-a-multi-instance-nodejs-service"></span>
    <a href="#installing-a-multi-instance-nodejs-service" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Another mistake beginners in the MEAN stack make is to start up a single Node.js instance. Yes, Node.js&rsquo;s asynchronous I/O nature makes it &ldquo;concurrent enough&rdquo; in 1 single thread. But you do want to maximize the rented machine so you should spin up at least one Node.js instance for each CPU core. A 1 GB RAM machine has just 1 core, but you can spin up at least 2 instances.</p>
<p>First of all, we must set up the <code>yourdomain.com</code> (<code>192.160.10.4</code> or <code>10.0.0.4</code> in the example) to support Node.js, let&rsquo;s do it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt-get install -y npm curl graphicsmagick nginx git bc
sudo npm install -g n
sudo npm install -g forever
sudo npm install -g forever-service
sudo n 0.10.40</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you haven&rsquo;t already, <a href="https://www.digitalocean.com/community/tutorials/how-to-create-a-sudo-user-on-ubuntu-quickstart"target="_blank" rel="noopener">create a more restricted sudo user</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>adduser rocket # add a password when asked
usermod -aG sudo rocket
su - rocket # and always ssh in with `ssh rocket@yourdomain.com`</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Within rocket&rsquo;s home directory let&rsquo;s install the Rocket.chat codebase:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cd /home/rocket
curl -L https://rocket.chat/releases/latest/download -o rocket.chat.tgz
tar zxvf rocket.chat.tgz
mv bundle Rocket.Chat
cd Rocket.Chat/programs/server
npm install</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And now let&rsquo;s add a way for the machine to start the Rocket.chat whenever it reboots:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cd ~/Rocket.Chat
sudo forever-service install -s main.js -e &#34;ROOT_URL=https://rocketchat42.com/ MONGO_URL=mongodb://mongo1.yourdomain.com:27017/rocketchat MONGO_OPLOG_URL=mongodb://mongo1.yourdomain.com:27017/local PORT=3001&#34; rocketchat1
sudo forever-service install -s main.js -e &#34;ROOT_URL=https://rocketchat42.com/ MONGO_URL=mongodb://mongo1.yourdomain.com:27017/rocketchat MONGO_OPLOG_URL=mongodb://mongo1.yourdomain.com:27017/local PORT=3002&#34; rocketchat2

sudo start rocketchat1
sudo start rocketchat2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Let&rsquo;s also create SSL certificates (and for that you must have a properly registered domain, of course). Again, <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04"target="_blank" rel="noopener">Digital Ocean&rsquo;s documentation on Let&rsquo;s Encrypt</a> is very good. In summary, all you have to do is:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now edit the <code>/etc/nginx/sites-available/default</code> and add this inside the server block:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>server {
  ...
  location ~ /.well-known {
          allow all;
  }
  ...
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you can:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service nginx reload

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

cd /opt/letsencrypt
./letsencrypt-auto certonly -a webroot --webroot-path=/usr/share/nginx/html -d yourdomain.com -d www.yourdomain.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Again, replace <code>yourdomain.com</code> with your own registered domain, of course. The <code>letsencrypt</code> command will prompt you to enter your e-mail address and accept terms of service.</p>
<p><a href="https://letsencrypt.org/"target="_blank" rel="noopener">Let&rsquo;s Encrypt</a> is a fantastic free SSL provider. Their certificates are short lived and are meant to expire in 90 days, so you must set up auto-renewal. Start <code>sudo crontab -e</code> and add the following lines:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>30 2 * * 1 /opt/letsencrypt/letsencrypt-auto renew &gt;&gt; /var/log/le-renew.log
35 2 * * 1 /etc/init.d/nginx reload</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should take care of SSL. Now you can edit your <code>/etc/nginx/sites-available/default</code> and replace it with this template:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Upstreams
upstream backend {
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
  listen 443 ssl;

  server_name yourdomain.com;

  ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  ssl_ciphers &#39;ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH&#43;AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA&#39;;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_stapling on;
  ssl_stapling_verify on;
  add_header Strict-Transport-Security max-age=15768000;

  location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &#34;upgrade&#34;;
    proxy_set_header Host $http_host;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forward-Proto http;
    proxy_set_header X-Nginx-Proxy true;

    proxy_redirect off;
  }

}

server {
  listen 80;
  server_name yourdomain.com www.yourdomain.com;
  return 301 https://$host$request_uri;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Just for the sake of completeness, again: replace <code>yourdomain.com</code> with your domain. Usually the <code>/etc/nginx/sites-enabled/default</code> is a symlink to <code>/etc/nginx/sites-available/default</code>, just check that as well. This configuration will load balance between the 2 node.js instances we configured and started before. And you can add as many as you want following the same <code>forever-install</code> procedure and adding the new instances to the <code>upstream</code> section in the nginx configuration above.</p>
<p>If everything is correct, you can restart nginx again: <code>sudo service nginx restart</code>.</p>
<p>And now you should have <code>https://yourdomain.com</code> already up and running. The first user that signs up will be the site administrator and from there you can customize Rocket.chat internally.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>As you can see, this is not a simple procedure to follow and I am assuming you have experience managing your own infrastructure. If you don&rsquo;t, you definitely should NOT do this by yourself.</p>
<p>If you do it correctly, you should have a functional Slack-clone with minimal reliability (thanks in large part to Digital Ocean) and minimal cost (USD 80/month in the configuration I described that should be enough for more than 100 users). Rocket.chat also offers more <a href="https://rocket.chat/docs/installation/"target="_blank" rel="noopener">documentation</a> for other environments including Docker configurations you might want to try. But the procedure above is sufficient for my needs.</p>
<p>I said it already but it&rsquo;s better to repeat it: don&rsquo;t fall for the trap of installing everything in a single box, with single instance MongoDB, and without proper monitoring. It&rsquo;s asking for trouble.</p>
<p>For now I am in the middle of the rollout. All developers in my company are already in the new deployment and soon half of the clients should also migrate (some will not be able to leave Slack just yet), but with Franz and the ability of full history and searchability this shouldn&rsquo;t be a concern even if a developer stays offline for some period of time.</p>
<p>I also don&rsquo;t advocate that everybody should be online and responding in real-time. It&rsquo;s infeasible, unproductive, creates unnecessary tension. People should participate when they have free time, and they should be able to concentrate without worrying that they are missing something. That&rsquo;s why they should opt-out of being notified in the more busy channels and just enable notification on the private groups that matter.</p>
<p>Coincidentally Jason Fried just posted about concerns over group chats at <a href="https://m.signalvnoise.com/is-group-chat-making-you-sweat-744659addf7d#.3qxe09una"target="_blank" rel="noopener">Signal v. Noise</a>, but the gist is that group chat should be purposeful and not yet another tool to create tension. People should definitely get offline when they need to fully concentrate on their work and have the opportunity to catch up with interesting conversations later. And really important communication should go through e-mail or other traditional ways, a simple <code>@all</code> doesn&rsquo;t cut it for company-wide announcements for example.</p>
<p>I hope this exercise gives you more perspective on what you can have and also raises awareness on the need for companies to regain more control over their own data, particularly knowledge. Erase your communication channels and you&rsquo;re losing years&rsquo; worth of knowledge that can be invaluable.</p>
]]></content:encoded><category>reviews</category><category>devops</category><category>databases</category></item></channel></rss>