[{"data":1,"prerenderedAt":151},["Reactive",2],{"tag-engineering":3,"featurePages":14,"tagPosts-engineering":15},{"slug":4,"id":5,"name":6,"description":7,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":10,"meta_description":11,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":12,"accent_color":8,"url":13},"engineering","6308cfbf730b190d1c2d7f42","Engineering","We're always experimenting with new features and complex solutions, as well as improving our existing offerings. Read about how we do it, directly from Chatwoot engineers.",null,"public","Engineering Blog | Learnings, tips, stories from Chatwoot Engineers","We're always experimenting with new features, & complex solutions, & improving our existing offerings. Read about how we do it – written by our engineers.","https://www.chatwoot.com/tags/engineering","https://www-internal-blog.chatwoot.com/tag/engineering/",[],[16,44,68,96,126],{"id":17,"uuid":18,"title":19,"slug":20,"html":21,"comment_id":17,"feature_image":22,"featured":23,"visibility":9,"created_at":24,"updated_at":25,"published_at":26,"custom_excerpt":8,"codeinjection_head":8,"codeinjection_foot":8,"custom_template":8,"canonical_url":8,"authors":27,"tags":36,"primary_author":38,"primary_tag":39,"url":40,"excerpt":41,"reading_time":42,"access":43,"comments":23,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":8,"meta_description":8,"email_subject":8,"frontmatter":8,"feature_image_alt":8,"feature_image_caption":8},"698dc19b4a1c2004e083a938","d73b3273-9661-4d3a-8690-8926f878ddcf","Incident Report - February 4, 2026","incident-report-february-4","\u003Cp>Chatwoot Cloud experienced an incident on February 4 which lasted approximately 12 minutes, from 12:48pm to 01:00pm UTC. During this time, all Chatwoot Cloud users were unable to access the platform. No data was lost during this incident.\u003C/p>\u003Cp>Our sincerest apologies for the disruption. Reliability is the top priority for us at Chatwoot. We have identified the risks and have taken steps to mitigate such events in future.\u003C/p>\u003Cp>\u003Cstrong>Timeline\u003C/strong>\u003C/p>\u003Cp>February 4, 2026\u003C/p>\u003Cul>\u003Cli>12:43 PM: Database instability began, connections started failing\u003C/li>\u003Cli>12:48 PM: Service disruption began, team started investigating\u003C/li>\u003Cli>12:58 PM: Root cause identified as storage exhaustion and Storage capacity increase initiated\u003C/li>\u003Cli>1:00 PM: Storage scaling completed, service fully restored\u003C/li>\u003C/ul>\u003Cp>\u003Cem>All times are in Coordinated Universal Time (UTC)\u003C/em>\u003C/p>\u003Cp>\u003Cstrong>What happened\u003C/strong>\u003C/p>\u003Cp>We were preparing a PostgreSQL version upgrade using AWS RDS blue-green deployments. The deployment failed, but it remained in a pending state and was not cleaned up.\u003C/p>\u003Cp>RDS blue-green deployments rely on logical replication. When the failed deployment was left behind, it retained a replication slot on the primary database. That replication slot prevented PostgreSQL from recycling write-ahead log (WAL) files.\u003C/p>\u003Cp>As a result, WAL files continued accumulating over three days. We had roughly 1 TB of actual data, but an additional ~1 TB of WAL built up, pushing us to our 2 TB storage autoscaling limit.\u003C/p>\u003Cp>Once storage was fully exhausted, the database stopped accepting connections, which caused the service disruption. After identifying the root cause, we immediately increased the storage capacity and restored service.\u003C/p>\u003Cp>\u003Cstrong>Follow-Up Actions and Preventive Measures\u003C/strong>\u003C/p>\u003Cp>To prevent similar incidents, we are implementing the following changes:\u003C/p>\u003Cul>\u003Cli>\u003Cu>Proactive storage monitoring\u003C/u>: We are adding alerts at multiple storage utilization thresholds (60%, 75%, 90%) to catch capacity issues before they become critical.\u003C/li>\u003Cli>\u003Cu>Replication slot monitoring\u003C/u>: We are implementing monitoring for database replication slots to detect orphaned slots that could cause WAL accumulation.\u003C/li>\u003Cli>\u003Cu>Database maintenance runbooks\u003C/u>: We are creating detailed runbooks for database upgrade procedures with mandatory cleanup steps when deployments fail.\u003C/li>\u003Cli>\u003Cu>Infrastructure capacity review\u003C/u>: We are reviewing storage limits and autoscaling configurations across all production systems to ensure adequate headroom.\u003C/li>\u003C/ul>","https://www-internal-blog.chatwoot.com/content/images/2026/02/Introduction---Figma-Thumbnail.png",false,"2026-02-12T12:03:39.000+00:00","2026-02-23T20:08:11.000+00:00","2026-02-10T12:08:00.000+00:00",[28],{"id":29,"name":30,"slug":31,"profile_image":32,"cover_image":8,"bio":8,"website":33,"location":8,"facebook":8,"twitter":34,"meta_title":8,"meta_description":8,"url":35},"611a190f4b8f26503f72d6a7","Vishnu Narayanan","vishnu","//www.gravatar.com/avatar/87734580b02f2def07ea9e48cc089466?s=250&d=mm&r=x","https://vishnunarayanan.com","@v_shnu","https://www-internal-blog.chatwoot.com/author/vishnu/",[37],{"id":5,"name":6,"slug":4,"description":7,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":10,"meta_description":11,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":12,"accent_color":8,"url":13},{"id":29,"name":30,"slug":31,"profile_image":32,"cover_image":8,"bio":8,"website":33,"location":8,"facebook":8,"twitter":34,"meta_title":8,"meta_description":8,"url":35},{"id":5,"name":6,"slug":4,"description":7,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":10,"meta_description":11,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":12,"accent_color":8,"url":13},"https://www-internal-blog.chatwoot.com/incident-report-february-4/","Chatwoot Cloud experienced an incident on February 4 which lasted approximately 12 minutes, from 12:48pm to 01:00pm UTC. During this time, all Chatwoot Cloud users were unable to access the platform. No data was lost during this incident.\n\nOur sincerest apologies for the disruption. Reliability is the top priority for us at Chatwoot. We have identified the risks and have taken steps to mitigate such events in future.\n\nTimeline\n\nFebruary 4, 2026\n\n * 12:43 PM: Database instability began, connectio",1,true,{"id":45,"uuid":46,"title":47,"slug":48,"html":49,"comment_id":45,"feature_image":50,"featured":23,"visibility":9,"created_at":51,"updated_at":52,"published_at":53,"custom_excerpt":54,"codeinjection_head":8,"codeinjection_foot":8,"custom_template":8,"canonical_url":8,"authors":55,"tags":62,"primary_author":64,"primary_tag":65,"url":66,"excerpt":54,"reading_time":67,"access":43,"comments":23,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":8,"meta_description":8,"email_subject":8,"frontmatter":8,"feature_image_alt":8,"feature_image_caption":8},"663913184a1c2004e083a2e3","5f75d606-e121-4c3c-991d-4a01f29dbc5f","Incident Report - April 16","incident-report-april-16","\u003Cp>\u003C/p>\u003Cp>Chatwoot experienced an incident on April 16 which lasted from 01:15pm to 03:24pm UTC. A spike in requests led to degraded services, causing some message events to be missed. This issue was not system-wide and impacted less than 1% of our active accounts, with fewer than 10 conversations affected in the majority of these accounts.\u003C/p>\u003Cp>Our sincerest apologies for the disruption. Reliability is the top priority for us at Chatwoot. We have identified the risks and have taken steps to mitigate such events in future.\u003C/p>\u003Ch3 id=\"timeline\">Timeline\u003C/h3>\u003Cp>\u003Cem>All times are in Coordinated Universal Time (UTC)\u003C/em>\u003C/p>\u003Cp>\u003Cstrong>April 16, 2024\u003C/strong>\u003C/p>\u003Cul>\u003Cli>1:15 PM: The high throughput alert (P1) for Sidekiq began, and the team started investigating the incident.\u003C/li>\u003Cli>1:21PM: The queue was not draining quickly enough, so the incident was escalated to P0. Queue latency exceeded 5s.\u003C/li>\u003Cli>1:40PM: The team identified the account causing the issue, initially attributing it to high website traffic.\u003C/li>\u003Cli>2:00PM: More workers were added to process the queues faster, but new jobs continued to be added rapidly.\u003C/li>\u003Cli>2:36 PM: The developer from the problematic account contacted us, revealing a bug in their system that caused a loop in the API calls.\u003C/li>\u003Cli>3:08PM: The account causing the issue was suspended.\u003C/li>\u003Cli>3:24PM: The system returned to stability.\u003C/li>\u003Cli>4:15PM: Some users reported missing messages in their conversations.\u003C/li>\u003Cli>5:00PM: The team discovered some jobs had gone missing during the incident.\u003C/li>\u003C/ul>\u003Cp>\u003Cstrong>April 17, 2024\u003C/strong>\u003C/p>\u003Cul>\u003Cli>The data lost during the event has been recovered. Our team has alerted the affected accounts describing the steps to share the lost data and the incident was resolved.\u003C/li>\u003C/ul>\u003Ch3 id=\"what-happened\">\u003Cstrong>What happened\u003C/strong>\u003C/h3>\u003Cp>We observed an increase in message creation requests, resulting in a backlog of background jobs. Although we tried to address this by adding more workers to process the queue, the backlog kept growing.\u003C/p>\u003Cp>We found that most of the background jobs were generated by a single account. We initially speculated that high website traffic and volume of conversations were to blame. However, we later identified a bug in the customer's system triggering the surge in message creation.\u003C/p>\u003Cp>This backlog resulted in a delay in message delivery, with latencies exceeding 5 seconds. However, after blocking the requests from the account, we were able to restore system back to a stable state.\u003C/p>\u003Cp>During our investigation into the incident, we discovered that some received messages were not created in Chatwoot. We have also received similar reports from customers. After examining the logs, we found a significant number of missing jobs. This was an unexpected behavior and the first time we saw such an issue.\u003C/p>\u003Cp>We queued the background jobs using Sidekiq, which utilizes Redis. In our Redis DB configuration, we set the eviction policy as \u003Ccode>allkeys-lru\u003C/code>. A surge in events resulted in a memory overflow, causing Redis to evict keys based on this policy. Due to this, we lost a lot of jobs which were yet to be executed.\u003C/p>\u003Cp>We were able to recover all of the missing message from our logs. Our initial plan was to restore as many messages as possible to their original conversations. However, we discovered that new messages have already been added to most conversations. As a result, importing the missing messages could lead to confusion for those managing the conversation.\u003C/p>\u003Cp>We notified the customers impacted by the incident and offered them with an option to get an excel export of the messages that were missed during the incident.\u003C/p>\u003Ch2 id=\"next-steps\">Next steps:\u003C/h2>\u003Cp>The incident allowed us to identify the shortcomings in our systems. As a result, we will be updating our systems as follows.\u003C/p>\u003Cul>\u003Cli>\u003Cu>Rate limiting for authenticated accounts:\u003C/u>\u003Cem> \u003C/em>We already had a rate limited for unauthenticated users, however we will be adding better rate-limiting constraints for authenticated users as well. This will include a limit on the number of messages a user can create within a specific time frame.\u003C/li>\u003Cli>\u003Cu>Moving the database backed background jobs\u003C/u>: We will be working towards a more persistent job queues for data-sensitive operations.\u003C/li>\u003C/ul>","https://www-internal-blog.chatwoot.com/content/images/2024/05/incident-report-thumbnail-1.png","2024-05-06T17:27:52.000+00:00","2024-05-06T17:45:32.000+00:00","2024-04-30T17:29:00.000+00:00","Chatwoot experienced an incident on April 16 which lasted from 01:15pm to 03:24pm UTC. A spike in requests led to degraded services, causing some message events to be missed. Read the detailed postmortem of the incident.",[56],{"id":57,"name":58,"slug":59,"profile_image":60,"cover_image":8,"bio":8,"website":8,"location":8,"facebook":8,"twitter":8,"meta_title":8,"meta_description":8,"url":61},"1","Pranav Raj S","pranav","https://www-internal-blog.chatwoot.com/content/images/2023/08/2246121.jpeg","https://www-internal-blog.chatwoot.com/author/pranav/",[63],{"id":5,"name":6,"slug":4,"description":7,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":10,"meta_description":11,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":12,"accent_color":8,"url":13},{"id":57,"name":58,"slug":59,"profile_image":60,"cover_image":8,"bio":8,"website":8,"location":8,"facebook":8,"twitter":8,"meta_title":8,"meta_description":8,"url":61},{"id":5,"name":6,"slug":4,"description":7,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":10,"meta_description":11,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":12,"accent_color":8,"url":13},"https://www-internal-blog.chatwoot.com/incident-report-april-16/",3,{"id":69,"uuid":70,"title":71,"slug":72,"html":73,"comment_id":69,"feature_image":74,"featured":23,"visibility":9,"created_at":75,"updated_at":76,"published_at":77,"custom_excerpt":78,"codeinjection_head":8,"codeinjection_foot":8,"custom_template":8,"canonical_url":8,"authors":79,"tags":86,"primary_author":92,"primary_tag":93,"url":94,"excerpt":78,"reading_time":95,"access":43,"comments":23,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":71,"meta_description":78,"email_subject":8,"frontmatter":8,"feature_image_alt":8,"feature_image_caption":8},"628363f74b8f26503f72dede","2e0dc686-fbcc-404f-b1a0-a40a17404d0c","How we improved Chatwoot File Upload","improving-chatwoot-file-upload","\u003Cp>\u003C/p>\u003Cp>Don't you hate waiting? When you know your internet connection is great, but the site takes a long time to load, or the uploaded files appear broken, or UI rendering is weird – it is frustrating. \u003C/p>\u003Cp>We ended up facing one of the above issues. Actually, not us, but a few of our customers. So yeah, us. :D \u003C/p>\u003Cfigure class=\"kg-card kg-image-card\">\u003Cimg src=\"https://www-internal-blog.chatwoot.com/content/images/2022/05/embarrased.gif\" class=\"kg-image\" alt loading=\"lazy\" width=\"499\" height=\"274\">\u003C/figure>\u003Cp>\u003C/p>\u003Cp>\u003C/p>\u003Ch2 id=\"heres-what-happened\">Here's what happened...\u003C/h2>\u003Cp>\u003C/p>\u003Cp>One of our customers was facing issues with uploading files. So, we dug in. \u003C/p>\u003Cp>One of the major problems in Chatwoot File upload was that the uploaded image appeared broken even if the upload was completed from the front end. \u003C/p>\u003Cp>This happened because uploading the attachment from our application servers to the cloud took time.\u003C/p>\u003Cp>\u003C/p>\u003Ch2 id=\"so-we-talked-about-possible-solutions\">So, we talked about possible solutions\u003C/h2>\u003Cp>\u003C/p>\u003Cp>We considered various methods to improve file upload in Chatwoot:\u003C/p>\u003Cp>\u003Cstrong>Uploading the attachment on the front end and saving it to the cloud from the sidekick\u003C/strong>\u003C/p>\u003Cp>But then delaying the upload would also create a similar issue – we won't be able to fetch/update or delete the message with an attachment unless the upload is completed. So this would bring us back to square one.\u003C/p>\u003Cp>\u003Cstrong>Using \u003Cem>\u003Ccode>aws-sdk-s3\u003C/code>\u003C/em> direct upload, where we send a request to the Amazon S3 cloud with our credentials. \u003C/strong>\u003C/p>\u003Cp>The S3 provides the pre-signed URL to upload the file, the solution which would work because they came with the pre-signed URL just for this problem. \u003C/p>\u003Cp>In one of our previous projects, we had seen a performance improvement with the pre-signed URL issue. Still, we discarded this idea because it is limited to S3 cloud storage, and we can't expect our self-hosted customers to stick to one cloud storage.\u003C/p>\u003Cp>\u003Cstrong>Rails active storage direct upload\u003C/strong>\u003C/p>\u003Cp>We decided to go with this, but wait. \u003C/p>\u003Cp>Rails let you implement this in just three steps, and it works like a charm for all the cloud storage providers supported by Chatwoot.\u003C/p>\u003Col>\u003Cli>Send the request to the \u003Ccode>direct_upload\u003C/code> endpoint provided by rails which is a \u003Ccode>direct_upload_controller\u003C/code>.\u003C/li>\u003Cli>Rails will return the authorized signed key to your cloud storage by reading your \u003Cem>\u003Ccode>storage.yml\u003C/code>.\u003C/em>\u003C/li>\u003Cli>Upload the file directly to the storage with the signed key.\u003C/li>\u003C/ol>\u003Cp>There is no intermediate application server to slow down the upload process.\u003C/p>\u003Cp>Great! This should solve our issue for sure, right?\u003C/p>\u003Cp>But as we progressed with the implementations, we encountered quite a few hurdles. \u003C/p>\u003Cul>\u003Cli>How are we authenticating the user? \u003C/li>\u003Cli>How are we ensuring that the request comes from an authorized user in Chatwoot? \u003C/li>\u003Cli>What if the signed key gets compromised? \u003C/li>\u003Cli>The cloud storage can get piled up, especially if someone bulk uploaded to the storage with many files in the given expiry time.\u003C/li>\u003C/ul>\u003Cp>\u003C/p>\u003Ch2 id=\"we-broke-the-issue-down\">We broke the issue down\u003C/h2>\u003Cp>1.\tWe couldn't use \u003Ccode>rails/active_storage/direct_uploads\u003C/code> endpoint as it doesn't have any authentication check and is extended from the action controller base.\u003C/p>\u003Cp>\u003Cu>Proposed Solution\u003C/u>: We need a separate endpoint for direct upload with the \u003Cem>\u003Ccode>rails/direct_upload\u003C/code>\u003C/em> functionalities.\u003C/p>\u003Cp>2.\tWe should be able to send the auth token with the direct upload signed key request. But it wasn't available with the current DirectUpload JS library and the documentation. \u003C/p>\u003Cp>\u003Cu>Proposed Solution\u003C/u>: We can be more ambitious and build out the extension to the existing library.\u003Cbr>\u003C/p>\u003Ch3 id=\"lets-solve-it-one-by-one\">Let's solve it one by one\u003C/h3>\u003Cp>\u003C/p>\u003Cp>\u003Cstrong>1.\t\u003Cstrong>\u003Cstrong>Add a custom endpoint to get the direct upload signed key\u003C/strong>\u003C/strong>\u003C/strong>\u003C/p>\u003Cp>The first issue and solution were easy to build and could be handled by the current \u003Ccode>direct_upload.js\u003C/code> library.\u003C/p>\u003Cp>We wrote a new controller where we extended \u003Cem>\u003Ccode>ActiveStorage::DirectUploadsController\u003C/code>\u003C/em> and added the logic to check if the user is a valid user asking for the signed key.\u003C/p>\u003Cfigure class=\"kg-card kg-code-card\">\u003Cpre>\u003Ccode class=\"language-ruby\">class Api::V1::Widget::DirectUploadsController &lt; ActiveStorage::DirectUploadsController\n  include WebsiteTokenHelper\n  before_action :set_web_widget\n  before_action :set_contact\n\n  def create\n    return if @contact.nil? || @current_account.nil?\n\n    super\n  end\nend\u003C/code>\u003C/pre>\u003Cfigcaption>Reference: \u003Ca href=\"https://github.com/chatwoot/chatwoot/blob/62d60f000bda12fb17876fcea39e8527c52feec9/app/controllers/api/v1/widget/direct_uploads_controller.rb?ref=www-internal-blog.chatwoot.com\">direct_uploads_controller.rb\u003C/a>\u003C/figcaption>\u003C/figure>\u003Cpre>\u003Ccode class=\"language-ruby\">class Api::V1::Widget::DirectUploadsController &lt; ActiveStorage::DirectUploadsController\n  include WebsiteTokenHelper\n  before_action :set_web_widget\n  before_action :set_contact\n\n  def create\n    return if @contact.nil? || @current_account.nil?\n\n    super\n  end\nend\u003C/code>\u003C/pre>\u003Cp>We updated the endpoint with the new controller in the new DirectUpload call.\u003C/p>\u003Cpre>\u003Ccode>const upload = new DirectUpload(file, url)\u003C/code>\u003C/pre>\u003Cp>So the first problem was solved. \u003C/p>\u003Cp>We had a new endpoint for direct upload. Hitting the new endpoint from the front end, the controller had a check for the website token and authentication token.\u003C/p>\u003Cp>\u003Cstrong>2.\t\u003Cstrong>\u003Cstrong>Authenticating the direct upload signed key request.\u003C/strong>\u003C/strong>\u003C/strong>\u003C/p>\u003Cp>Now let's move on to the second issue, how could we send the website token and authentication token from the front end to the new endpoint in the new DirectUpload call?\u003C/p>\u003Cp>We checked the \u003Ca href=\"https://edgeguides.rubyonrails.org/active_storage_overview.html?ref=www-internal-blog.chatwoot.com#direct-uploads\">documentation\u003C/a>. Was there a way to send extra data with the request? We couldn't find any way. \u003C/p>\u003Cp>We checked if there was an event on which we could bind the token, but none was mentioned. There were just file and URL parameters that you could send to DirectUpload class.\u003C/p>\u003Cpre>\u003Ccode class=\"language-javascript\">const url = input.dataset.directUploadUrl\nconst upload = new DirectUpload(file, url)\n\nupload.create((error, blob) =&gt; {\n if (error) {\n   // Handle the error\n } else {\n   // Add an appropriately-named hidden input to the form with a\n   //  value of blob.signed_id so that the blob ids will be\n   //  transmitted in the normal upload flow\n   const hiddenField = document.createElement('input')\n   hiddenField.setAttribute('type', 'hidden')\n   hiddenField.setAttribute('value', blob.signed_id)\n   hiddenField.name = input.name\n   document.querySelector('form').appendChild(hiddenField)\n }\n})\u003C/code>\u003C/pre>\u003Cp>\u003Cbr>At one point, we thought about extending the \u003Cem>\u003Ccode>DirectUpload.js\u003C/code>\u003C/em> and making our \u003Cstrong>Chatwoot\u003C/strong> version. But, then we thought: let's open the library first, and hey! So there we had it; there was an event bound to creating a blob process when we sent the request to get the signed key. The event was \u003Cem>\u003Cstrong>\u003Ccode>DirectUploadWillCreateBlobWithXHR\u003C/code>\u003C/strong>\u003C/em>.\u003C/p>\u003Cp>You can call this event while sending the parameters to DirectUpload and add the new tokens in XHR headers.\u003C/p>\u003Cp>Here is how we do it:\u003C/p>\u003Cp>1) We are sending the website token in the URL\u003C/p>\u003Cp>2) Setting the auth token in the XHR header, and\u003C/p>\u003Cp>3) Sending it to the new URL endpoint.\u003C/p>\u003Cpre>\u003Ccode class=\"language-javascript\">const { websiteToken } = window.chatwootWebChannel;\nconst upload = new DirectUpload(\n  file.file,\n  `/api/v1/widget/direct_uploads?website_token=${websiteToken}`,\n  {\n    directUploadWillCreateBlobWithXHR: xhr =&gt; {\n      xhr.setRequestHeader('X-Auth-Token', window.authToken);\n    },\n  }\n);\n \nupload.create((error, blob) =&gt; {\n  if (error) {\n    window.bus.$emit(BUS_EVENTS.SHOW_ALERT, {\n      message: error,\n    });\n  } else {\n    this.onAttach({\n      file: blob.signed_id,\n      ...this.getLocalFileAttributes(file),\n    });\n  }\n});\u003C/code>\u003C/pre>\u003Cp>I think we met the criteria to resolve the second issue.\u003C/p>\u003Ch2 id=\"to-conclude\">To conclude\u003C/h2>\u003Cp>With both the issues resolved, we were good to go with this feature, which is already a bug fix for one of our customers. :D\u003C/p>\u003Cp>Long story short, rails documentation is not up to the mark, but the feature is, and there is a way to authenticate your direct upload request with this active storage's new feature.\u003C/p>\u003Cp>And you can always refer to our blog if you are stuck at direct upload.\u003C/p>\u003Cp>Here are the links to related GitHub issues:\u003C/p>\u003Cp>https://github.com/chatwoot/chatwoot/issues/3567 https://github.com/chatwoot/chatwoot/issues/4147\u003C/p>\u003Cp>Thanks for reading. ✌️\u003Cbr>\u003C/p>","https://www-internal-blog.chatwoot.com/content/images/2022/07/Chatwoot-engineering.png","2022-05-17T08:59:35.000+00:00","2022-07-29T05:09:58.000+00:00","2022-07-28T16:32:08.000+00:00","We were facing some issues with faulty file upload in Chatwoot. How did we take this issue, and how did we finally fix it? Here's a full account!",[80],{"id":81,"name":82,"slug":83,"profile_image":84,"cover_image":8,"bio":8,"website":8,"location":8,"facebook":8,"twitter":8,"meta_title":8,"meta_description":8,"url":85},"628363ae4b8f26503f72dedb","Tejaswini Chile","tejaswini","https://www-internal-blog.chatwoot.com/content/images/2022/07/tejaswini.jpeg","https://www-internal-blog.chatwoot.com/author/tejaswini/",[87,91],{"id":88,"name":89,"slug":89,"description":8,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":8,"meta_description":8,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":8,"accent_color":8,"url":90},"6118da864b8f26503f72d530","blog","https://www-internal-blog.chatwoot.com/tag/blog/",{"id":5,"name":6,"slug":4,"description":7,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":10,"meta_description":11,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":12,"accent_color":8,"url":13},{"id":81,"name":82,"slug":83,"profile_image":84,"cover_image":8,"bio":8,"website":8,"location":8,"facebook":8,"twitter":8,"meta_title":8,"meta_description":8,"url":85},{"id":88,"name":89,"slug":89,"description":8,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":8,"meta_description":8,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":8,"accent_color":8,"url":90},"https://www-internal-blog.chatwoot.com/improving-chatwoot-file-upload/",5,{"id":97,"uuid":98,"title":99,"slug":100,"html":101,"comment_id":97,"feature_image":102,"featured":23,"visibility":9,"created_at":103,"updated_at":104,"published_at":105,"custom_excerpt":106,"codeinjection_head":8,"codeinjection_foot":8,"custom_template":8,"canonical_url":107,"authors":108,"tags":120,"primary_author":123,"primary_tag":124,"url":125,"excerpt":106,"reading_time":67,"access":43,"comments":23,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":99,"meta_description":8,"email_subject":8,"frontmatter":8,"feature_image_alt":8,"feature_image_caption":8},"610cd502b9956c0555c7f7a9","d45f93ca-ff1d-4e65-ae6b-88e9bb4fd8cb","Implementing Deep linking in React native mobile apps","implementing-deep-linking-in-react-native","\u003Cp>\u003Cem>Learnings from implementing Deep linking in Chatwoot mobile apps\u003C/em>\u003C/p>\u003Cp>We live in a world where things are interlinked. We share links more frequently than ever before and want our customers to reach their desired pages swiftly, regardless of their platforms. Deep links could help significantly in enhancing this experience.\u003C/p>\u003Ch2 id=\"what-is-deep-linking\">What is deep linking?\u003C/h2>\u003Cp>Deep linking enables a user to navigate to specific content in a mobile application using a URL.\u003C/p>\u003Cp>Deep links are web links that can activate your app and contain information needed to load specific app sections. They can be used as triggers in external events like \u003Cem>push notification, emails, web links etc.\u003C/em>\u003C/p>\u003Cp>They enable users to save more time and energy without locating a particular page by themselves – significantly improving the user experience.\u003C/p>\u003Cp>Note: The \"deep\" refers to the depth of the page in a hierarchical app structure of pages.\u003C/p>\u003Ch2 id=\"why-deep-linking\">Why deep linking?\u003C/h2>\u003Cp>Deeplinking enhances the user experience for mobile app users. Before implementing Deep linking In Chatwoot, a user on a mobile device clicking on a conversation link received via an email would be taken to the web browser, even if they have the Chatwoot app installed.\u003C/p>\u003Cp>With deep linking, the Chatwoot app can open the conversation screen based on the id in the URL. Then, when the user clicks on a link, it will open the app, and the user is navigated to the exact point in the app where they are destined. This is a much better user experience.\u003C/p>\u003Cfigure class=\"kg-card kg-image-card\">\u003Cimg src=\"https://www-internal-blog.chatwoot.com/content/images/2022/09/deeplinking-demo.gif\" class=\"kg-image\" alt loading=\"lazy\" width=\"600\" height=\"1299\" srcset=\"https://www-internal-blog.chatwoot.com/content/images/2022/09/deeplinking-demo.gif 600w\">\u003C/figure>\u003Ch2 id=\"how-to-implement-deep-linking\">How to implement Deep Linking?\u003C/h2>\u003Cp>Deep links can be implemented in ios and android by the following methods :\u003C/p>\u003Cul>\u003Cli>Custom URL scheme (iOS Universal Links)\u003C/li>\u003Cli>Intent URL (Android)\u003C/li>\u003C/ul>\u003Ch2 id=\"configuration-for-android\">Configuration for Android\u003C/h2>\u003Cp>Add an \u003Ca href=\"https://developer.android.com/guide/components/intents-filters?ref=www-internal-blog.chatwoot.com\">intent-filter\u003C/a> in AndroidManifest.xml to specify the host link\u003C/p>\u003Cpre>\u003Ccode>&lt;data android:scheme=\"https\" android:host=\"http://app.chatwoot.com/\" /&gt;\n\u003C/code>\u003C/pre>\u003Cp>Then build the project. To test this action, run the following command in your terminal.\u003C/p>\u003Cpre>\u003Ccode class=\"language-bash\">adb shell am start -W -a android.intent.action.VIEW -d \"https://app.chatwoot.com/app/accounts/1/conversations/12121\" com.chatwoot.app\n\u003C/code>\u003C/pre>\u003Cp>Now the app will open up successfully if everything went well.\u003C/p>\u003Ch2 id=\"configuration-for-ios\">Configuration for iOS\u003C/h2>\u003Cp>Apple recommends Universal links as the way to open your web links in your mobile app.\u003C/p>\u003Cp>To support universal linking in iOS, we need to add some configuration on the server-side as well. First, the endpoint must use HTTPS.\u003C/p>\u003Cp>Your server has to have a route that is defined as a get request with the path \u003Cstrong>/apple-app-site-association.\u003C/strong>. When a request is made to that path, you must return a file with the configuration recommended below.\u003C/p>\u003Cpre>\u003Ccode>{\n    \"applinks\": {\n        \"apps\": [],\n        \"details\": [\n            {\n                \"appID\": \"&lt;TeamID&gt;.&lt;Bundle-Identifier&gt;\",\n                \"paths\": [ \"NOT /super_admin/*\", \"*\" ]\n            }\n        ]\n    }\n}\n\u003C/code>\u003C/pre>\u003Ch2 id=\"implementation-on-the-server-side\">Implementation on the server-side\u003C/h2>\u003Cp>Apart from the configurations required in your mobile app, you would also need to implement some changes on your server for deep links to work for ios.\u003C/p>\u003Cp>Here is an example implementation for a ruby on rails server.\u003C/p>\u003Cp>\u003Cstrong>routes.rb\u003C/strong>\u003C/p>\u003Cpre>\u003Ccode class=\"language-ruby\">get 'apple-app-site-association' =&gt; 'apple_app#site_association'\n\u003C/code>\u003C/pre>\u003Cp>\u003Cstrong>app/controllers/apple_controller.rb\u003C/strong>\u003C/p>\u003Cpre>\u003Ccode class=\"language-ruby\">class AppleController &lt; ApplicationController\n  def site_association\n    site_association_json = render_to_string action: 'site_association', layout: false\n    send_data site_association_json, filename: 'apple-app-site-association', type: 'application/json'\n  end\nend\n\u003C/code>\u003C/pre>\u003Cp>\u003Cstrong>app/views/apple\u003Cem>app/site\u003C/em>association.html.erb\u003C/strong>\u003C/p>\u003Cpre>\u003Ccode class=\"language-ruby\">{\n    \"applinks\": {\n        \"apps\": [],\n        \"details\": [\n            {\n                \"appID\": \"&lt;%= ENV['IOS_APP_ID'] %&gt;\",\n                \"paths\": [ \"NOT /super_admin/*\", \"*\" ]\n            }\n        ]\n    }\n}\n\u003C/code>\u003C/pre>\u003Cp>Find the implementation of Universal links in Chatwoot in this \u003Ca href=\"https://github.com/chatwoot/chatwoot/pull/805?ref=www-internal-blog.chatwoot.com\">pull request\u003C/a>.\u003C/p>\u003Cp>Once your server endpoint is ready, Launch Xcode. Select the Signing &amp; \u003Cstrong>Capabilities\u003C/strong> tab and then select the enable \u003Cstrong>Associated Domains\u003C/strong> and add domains. Make sure to prefix the domain with \u003Ccode>applinks:\u003C/code> in place of \u003Ccode>https://\u003C/code>\u003C/p>\u003Cfigure class=\"kg-card kg-image-card\">\u003Cimg src=\"https://www.chatwoot.com/static/b85130d2d4fa3ed12bf6372cf618f580/16abd/deeplinking-xcode.png\" class=\"kg-image\" alt=\"deeplinking-xcode\" loading=\"lazy\" title=\"deeplinking-xcode\">\u003C/figure>\u003Cp>Find the implementation of Universal links in Chatwoot mobile app in this \u003Ca href=\"https://github.com/chatwoot/chatwoot-mobile-app/pull/297?ref=www-internal-blog.chatwoot.com\">pull request\u003C/a>.\u003C/p>\u003Ch2 id=\"final-steps\">Final Steps\u003C/h2>\u003Cp>Once our app is ready to handle deep Links, we need to implement actions to navigate the user to required screens. React native has \u003Ca href=\"https://reactnative.dev/docs/linking?ref=www-internal-blog.chatwoot.com\">Linking\u003C/a> module, which will provide API that allows us to listen for an incoming linked url.\u003C/p>\u003Cp>Linking gives you a general interface to interact with incoming app links. For example, if an app link triggered the app launch, it provides the link url. Otherwise, it will provide null. If we get the link url, it redirects to the exact screen based on the incoming URL.\u003C/p>\u003Cpre>\u003Ccode class=\"language-javascript\">useEffect(() =&gt; {\n // Get the deep link used to open the app\n   const initialUrl = await Linking.getInitialURL();\n // Redirect to particualr screen based on inital url\n   navigation.navigate('ChatScreen');\n}, []);\n\u003C/code>\u003C/pre>\u003Cp>Your mobile app should now be ready to handle deep links if you have followed the above instructions. Thank you for reading, and I hope you liked it. Please let us know if you have any doubts ;)\u003C/p>","https://www-internal-blog.chatwoot.com/content/images/2021/08/deeplinking-banner.png","2021-08-06T06:21:54.000+00:00","2022-09-20T09:24:38.000+00:00","2021-07-14T06:22:00.000+00:00","Learnings from implementing Deep linking in Chatwoot mobile apps\nWe live in a world where things are interlinked. We share links more frequently than ever before and want our customers to reach their desired pages...","https://www.chatwoot.com/blog/implementing-deep-linking-in-react-native",[109],{"id":110,"name":111,"slug":112,"profile_image":113,"cover_image":114,"bio":115,"website":116,"location":117,"facebook":8,"twitter":118,"meta_title":8,"meta_description":8,"url":119},"6118d8d14b8f26503f72d51b","Muhsin K","muhsin","https://www-internal-blog.chatwoot.com/content/images/2026/02/CleanShot-2026-02-23-at-12.44.10@2x.png","https://www-internal-blog.chatwoot.com/content/images/2021/08/1500x500.jpeg","Product-minded engineer building across backend, frontend, mobile and AI systems.","https://www.muhsi.me","Bengauluru, India","@muhsin_keloth","https://www-internal-blog.chatwoot.com/author/muhsin/",[121,122],{"id":88,"name":89,"slug":89,"description":8,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":8,"meta_description":8,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":8,"accent_color":8,"url":90},{"id":5,"name":6,"slug":4,"description":7,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":10,"meta_description":11,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":12,"accent_color":8,"url":13},{"id":110,"name":111,"slug":112,"profile_image":113,"cover_image":114,"bio":115,"website":116,"location":117,"facebook":8,"twitter":118,"meta_title":8,"meta_description":8,"url":119},{"id":88,"name":89,"slug":89,"description":8,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":8,"meta_description":8,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":8,"accent_color":8,"url":90},"https://www-internal-blog.chatwoot.com/implementing-deep-linking-in-react-native/",{"id":127,"uuid":128,"title":129,"slug":130,"html":131,"comment_id":127,"feature_image":132,"featured":23,"visibility":9,"created_at":133,"updated_at":134,"published_at":135,"custom_excerpt":136,"codeinjection_head":8,"codeinjection_foot":8,"custom_template":8,"canonical_url":137,"authors":138,"tags":145,"primary_author":148,"primary_tag":149,"url":150,"excerpt":136,"reading_time":67,"access":43,"comments":23,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":129,"meta_description":8,"email_subject":8,"frontmatter":8,"feature_image_alt":8,"feature_image_caption":8},"610cd46fb9956c0555c7f79b","4679c264-8e80-4af8-8875-ca2d0363364a","Access localhost from the internet – External service callbacks made easy with LocalTunnel","manage-external-service-callbacks-with-local-tunnel","\u003Cp>\u003Cem>Improve development experience with external service callbacks from Facebook, Twitter, Twilio etc., using LocalTunnel.\u003C/em>\u003C/p>\u003Ch2 id=\"working-with-third-party-integrations\">Working with third-party integrations\u003C/h2>\u003Cp>As a product developer, one of the biggest pain points that you come across might be related to working with callbacks (mostly webhooks) from third party systems in your local development environment. Most of us use some tunnelling software to expose our local server to the internet and then use that URL for the callbacks.\u003C/p>\u003Cp>There are some pain points with this setup. Usually, tunnelling software has this annoying side-effect of changing the exposed URL every time you restart. If you have shared it with your colleagues for testing, you would have to send the new URL again. If you are working on the webhook callbacks with third-party integrations, like Facebook, Twitter, Zapier, Twilio, etc., this becomes frustrating. Every time you restart the tunnel, you have to go to the developer console and update the callback URL. This workflow is cumbersome, and often we forget this and spend a good amount of time debugging on other things.\u003C/p>\u003Ch2 id=\"chatwoots-context\">Chatwoot's Context\u003C/h2>\u003Cp>At Chatwoot, we have multi-channel support and have inbox support for Facebook, Twitter, Twilio, Slack, chatbots, and various email platforms. We constantly struggled with this problem.\u003C/p>\u003Cp>One way to mitigate the problem was by getting individual paid accounts for every team member and get a static URL for the tunnel. This approach solved some of the issues as we don't need to update the callback URLs every time we restart the local system. For the entire period of development, the URL stays the same.\u003C/p>\u003Cp>Everything seemed good initially. After some time, we started running into problems again with this setup. When multiple engineers work on a single integration, they have to change the callback URL to their tunnel URL whenever they work on it. Nobody called dibs on this, and often we forget to check the callback URL when we start the work. We were on the path to expand our integrations as a part of the omnichannel support and we wanted a better development experience.\u003C/p>\u003Cp>We needed the API callback URLs to stay constant, and it should be easy for any developer to get hold of that tunnel URL. As a possible solution, we looked for open source tunnelling softwares that we could host and customize. That is when we came across LocalTunnel and fell in love with it.\u003C/p>\u003Ch2 id=\"what-is-localtunnel\">What is LocalTunnel?\u003C/h2>\u003Cp>LocalTunnel exposes your localhost to the world for easy testing and sharing. It is an open-source software. (\u003Ca href=\"https://github.com/localtunnel/localtunnel?ref=www-internal-blog.chatwoot.com\">https://github.com/localtunnel/localtunnel\u003C/a>). From the number of stars and forks of this project, you can see that the community loves this product.\u003C/p>\u003Ch2 id=\"how-did-we-set-it-up\">How did we set it up?\u003C/h2>\u003Cp>We decided to set up our own LocalTunnel on a Digital Ocean droplet. We followed \u003Ca href=\"https://medium.com/quark-works/running-your-own-reverse-proxy-with-localtunnel-b1658e239c35?ref=www-internal-blog.chatwoot.com\">this\u003C/a> excellent article written by Alex to host LocalTunnel on Digital Ocean. In addition to the setup guide, we had to open a range of ports on the droplet to allow TCP connections.\u003C/p>\u003Cp>We pointed one of the subdomains to the droplet that runs LocalTunnel, which looks something like \u003Ccode>https://tunnel.example.com\u003C/code>.\u003C/p>\u003Ch2 id=\"development-workflow\">Development Workflow\u003C/h2>\u003Cp>To start working with LocalTunnel, you have to install the LocalTunnel client on your machine using the following command.\u003C/p>\u003Cpre>\u003Ccode class=\"language-bash\">npm install -g localtunnel\n\u003C/code>\u003C/pre>\u003Cp>You can connect to the LocalTunnel server by specifying the port and a host name.\u003C/p>\u003Cpre>\u003Ccode class=\"language-bash\">lt --port 3000 --host http://tunnel.example.com\n\u003C/code>\u003C/pre>\u003Cp>By default, LocalTunnel would generate a subdomain using random characters. The URL would look like \u003Ccode>https://[random-characters].tunnel.example.com\u003C/code>\u003C/p>\u003Cp>In addition to this, LocalTunnel allows you to request a named subdomain on the LocalTunnel server.\u003C/p>\u003Cpre>\u003Ccode class=\"language-bash\">lt --port 3000 --host http://tunnel.example.com --subdomain customsubdomain\n\u003C/code>\u003C/pre>\u003Cp>This feature allowed us to standardize the list of subdomains to the external services as seen below:\u003C/p>\u003C!--kg-card-begin: html-->\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Service\u003C/th>\n\u003Cth>Subdomain\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Facebook\u003C/td>\n\u003Ctd>fb-dev.tunnel.example.com\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Sendgrid\u003C/td>\n\u003Ctd>sendgrid-dev.tunnel.example.com\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Mailgun\u003C/td>\n\u003Ctd>mailgun-staging.tunnel.example.com\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Twitter\u003C/td>\n\u003Ctd>twitter-staging.tunnel.example.com\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Twilio\u003C/td>\n\u003Ctd>twilio-dev.tunnel.example.com\u003C/td>\n\u003C/tr>\n\u003C/tbody>\n\u003C/table>\u003C!--kg-card-end: html-->\u003Cp>An important thing to note here is that when we use a subdomain, for example, Facebook development as \u003Ccode>fb-dev.tunnel.example.com\u003C/code>, we set all the callbacks on the Facebook developer account to this subdomain. Similarly, we set the callback URL on the Twitter developer account for Twitter app development to \u003Ccode>twitter-dev.tunnel.example.com\u003C/code> and so on.\u003C/p>\u003Cp>For working on Facebook inbox, we would connect to the standard tunnel URL:\u003C/p>\u003Cpre>\u003Ccode class=\"language-bash\">lt --host http://tunnel.example.com --subdomain fb-dev --port 3000\n\u003C/code>\u003C/pre>\u003Cp>With this approach, we solved both problems mentioned above.\u003C/p>\u003Col>\u003Cli>Now that you had a standard set of URLs, you don't have to go to the developer console every time to change it.\u003C/li>\u003Cli>Any team member can get hold of any of these tunnels using their LocalTunnel Client seamlessly.\u003C/li>\u003C/ol>\u003Cp>This approach has improved our development workflow to a great extend. The developers don't have to worry about the URL they would use in development. If they conform to the standard URL, everything works seamlessly.\u003C/p>\u003Cp>Let us know if you have any questions or if this article helped you in improving your development experience. We are all ears. :)\u003C/p>","https://www-internal-blog.chatwoot.com/content/images/2021/08/local_tunnel_ts.png","2021-08-06T06:19:27.000+00:00","2022-09-20T09:27:19.000+00:00","2021-05-06T06:20:00.000+00:00","Improve development experience with external service callbacks from Facebook, Twitter, Twilio etc., using LocalTunnel. Working with third-party integrations As a product developer, one of the biggest pain points that you come across might be related to working with…","https://www.chatwoot.com/blog/manage-external-service-callbacks-with-local-tunnel",[139],{"id":140,"name":141,"slug":142,"profile_image":143,"cover_image":8,"bio":8,"website":8,"location":8,"facebook":8,"twitter":8,"meta_title":8,"meta_description":8,"url":144},"6118d8e34b8f26503f72d51e","Sony Mathew","sony","https://www-internal-blog.chatwoot.com/content/images/2021/08/zQK4swoV_400x400.jpg","https://www-internal-blog.chatwoot.com/author/sony/",[146,147],{"id":88,"name":89,"slug":89,"description":8,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":8,"meta_description":8,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":8,"accent_color":8,"url":90},{"id":5,"name":6,"slug":4,"description":7,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":10,"meta_description":11,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":12,"accent_color":8,"url":13},{"id":140,"name":141,"slug":142,"profile_image":143,"cover_image":8,"bio":8,"website":8,"location":8,"facebook":8,"twitter":8,"meta_title":8,"meta_description":8,"url":144},{"id":88,"name":89,"slug":89,"description":8,"feature_image":8,"visibility":9,"og_image":8,"og_title":8,"og_description":8,"twitter_image":8,"twitter_title":8,"twitter_description":8,"meta_title":8,"meta_description":8,"codeinjection_head":8,"codeinjection_foot":8,"canonical_url":8,"accent_color":8,"url":90},"https://www-internal-blog.chatwoot.com/manage-external-service-callbacks-with-local-tunnel/",1775212113055]