<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Rohit Pujari</title>
    <description>The latest articles on Forem by Rohit Pujari (@rohitspujari).</description>
    <link>https://forem.com/rohitspujari</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F163326%2Fc82397e0-e460-4b41-8c7f-f3799cb33d62.png</url>
      <title>Forem: Rohit Pujari</title>
      <link>https://forem.com/rohitspujari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rohitspujari"/>
    <language>en</language>
    <item>
      <title>Supercharge your Retool apps with stunning dashboards</title>
      <dc:creator>Rohit Pujari</dc:creator>
      <pubDate>Tue, 08 Oct 2024 13:25:06 +0000</pubDate>
      <link>https://forem.com/rohitspujari/supercharge-your-retool-apps-with-stunning-dashboards-1fgg</link>
      <guid>https://forem.com/rohitspujari/supercharge-your-retool-apps-with-stunning-dashboards-1fgg</guid>
      <description>&lt;p&gt;Analytics is a must have for modern applications, whether they're internal or customer-facing. This is especially true for enterprise apps that manage internal business processes. Take, for example, an app that allows employees to submit support requests. For these types of business process management applications, it's crucial to have visibility at both individual and aggregate levels. At the individual level, users can leverage peer insights to make informed decisions. At the aggregate level, supervisors can better understand where requests are coming from, identify bottlenecks, and uncover areas for improvement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://retool.com" rel="noopener noreferrer"&gt;Retool&lt;/a&gt; is an excellent platform for building departmental apps. It enables you to quickly create, publish, and share enterprise apps without going through layers of IT approvals. When it comes to analytics, Retool provides some in-line charts to show quick insights. For simple use cases, this might be sufficient. However, if you're managing a critical business process that requires you to deliver tailored visibility at multiple levels of organization —where one size doesn't fit all—you'll likely want a personalized dashboard that caters to each stakeholder group.&lt;/p&gt;

&lt;p&gt;The most efficient approach you can take to meet this requirement is by decoupling analytics from your core application code. This strategy offers several advantages. Firstly, it substantially enhances your iteration velocity, allowing for rapid adjustments and improvements to your analytics without affecting the main application. Secondly, it enables you to deploy changes much more quickly than traditional integrated approaches. This separation of concerns leads to increased flexibility and maintainability of both your application and analytics components. Furthermore, it allows specialized teams to work on each part independently, potentially improving overall quality and innovation.&lt;/p&gt;

&lt;p&gt;This is where the Sempahor + Retool integration shines. You can now effortlessly launch fully interactive dashboards within your Retool apps in a few simple steps. This approach not only streamlines the development workflow but also ensures your analytics capabilities can grow with your application needs, minimizing friction. Let's see it in action.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Gp6XFlUUv48"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Request a free trial &lt;a href="https://semaphor.cloud/home" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you log in, you'll see a demo dashboard created for you. With Semaphor, you can connect to any SQL-compatible databases, including CSV files, Parquet files in Amazon S3, or responses from REST APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Create a dashboard using natural language, SQL, or Python.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Semaphor console allows you to customize the dashboard's appearance—including colors, fonts, corner styles, and layouts for various devices—giving you precise control over how it renders in your app.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/BGOB6Ko_IFM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Publish it as a custom component to your Retool App.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clone the template repository and install dependencies&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;git clone https://github.com/rohitspujari/retool-semaphor-component.git
cd retool-semaphor-component
npm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize the component library&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;npx retool-ccl init

npx retool-ccl init
? What name would you like to give your library of components? semaphor
? What description would you like to give ? make my data dance and insights shine

Creating new library in the Retool backend..
...Succesfully made new library!
Writing updates to package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the component in dev mode, and go to the Retool console. You can then search for &lt;code&gt;seamphor&lt;/code&gt; in the components section and drag it onto your canvas.&lt;/p&gt;

&lt;p&gt;In the component properties, provide semaphor &lt;code&gt;dashboard Id&lt;/code&gt; and &lt;code&gt;secret&lt;/code&gt; to render the dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx retool-ccl dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you are happy with the template, you can publish it to Retool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx retool-ccl deploy

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! You now have a fully interactive dashboard running inside your Retool app within minutes. If you have any questions feel free to drop us a note at &lt;a href="mailto:support@semaphor.cloud"&gt;support@semaphor.cloud&lt;/a&gt;&lt;/p&gt;

</description>
      <category>retool</category>
      <category>react</category>
      <category>dashboards</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Multi-tenant analytics for Next.js Apps</title>
      <dc:creator>Rohit Pujari</dc:creator>
      <pubDate>Sun, 22 Sep 2024 15:42:48 +0000</pubDate>
      <link>https://forem.com/rohitspujari/multi-tenant-analytics-for-nextjs-apps-3d1h</link>
      <guid>https://forem.com/rohitspujari/multi-tenant-analytics-for-nextjs-apps-3d1h</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/rohitspujari/fully-interactive-analytics-for-your-nextjs-app-49dh"&gt;previous blog post&lt;/a&gt;, we explored how you can render interactive analytics in your Next.js app with just a few lines of code.&lt;/p&gt;

&lt;p&gt;In this post, we'll discuss how you can deliver a multi-tenant analytics experience in your Next.js app. Specifically, we'll examine how to serve the same dashboard to multiple tenants (clients or organizations) while keeping each tenant's data isolated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://semaphor.cloud" rel="noopener noreferrer"&gt;Semaphor&lt;/a&gt; offers two ways to implement multi-tenancy: &lt;code&gt;connection-level security&lt;/code&gt; and &lt;code&gt;row-level security&lt;/code&gt;. Connection-level security allows you to modify parts of the connection string (e.g., database name or user) at runtime. This is ideal if you have dedicated databases, database users, or folders (in the case of Amazon S3) for each client. Row-level security, on the other hand, provides logical isolation within a single shared table, displaying only the rows relevant to each tenant.&lt;/p&gt;

&lt;p&gt;In this post we’ll focus on multi-tenancy using &lt;code&gt;row-level security&lt;/code&gt;. Imagine you're a SaaS company providing embedded dashboards to multiple tenants, such as &lt;code&gt;Tenant A&lt;/code&gt;, &lt;code&gt;Tenant B&lt;/code&gt;, and &lt;code&gt;Tenant C&lt;/code&gt;. These companies access their dashboards from within your product, but they can see only their own data, depending on the tenancy and the user.&lt;/p&gt;

&lt;p&gt;For illustration, let's assume a simple data isolation scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tenant A users can only see data for eastern US states (NY, CT, FL)&lt;/li&gt;
&lt;li&gt;Tenant B users can see data for central US states (IL, TX, OH)&lt;/li&gt;
&lt;li&gt;Tenant C users can see data for western US states (CA, WA, NV)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.semaphor.cloud%2F_next%2Fimage%3Furl%3D%252Fimages%252Fmulti-tenancy.gif%26w%3D1200%26q%3D75" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.semaphor.cloud%2F_next%2Fimage%3Furl%3D%252Fimages%252Fmulti-tenancy.gif%26w%3D1200%26q%3D75" alt="Multi-tenant Demo" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's see how we can implement this in Semaphor.&lt;/p&gt;

&lt;p&gt;Before we begin, we assume that you already know how to integrate Semaphor dashboard into your Next.js app, or you can follow the instructions &lt;a href="https://dev.to/rohitspujari/fully-interactive-analytics-for-your-nextjs-app-49dh"&gt;here&lt;/a&gt; to bootstrap a demo app.&lt;/p&gt;

&lt;p&gt;To get started quickly, just clone this &lt;a href="https://github.com/rohitspujari/nextjs-semaphor-rls-demo" rel="noopener noreferrer"&gt;Git repository&lt;/a&gt; and run it locally. You’ll have a demo dashboard with row-level security set up in no time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;git&lt;/span&gt; &lt;span class="nx"&gt;clone&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//github.com/rohitspujari/nextjs-semaphor-rls-demo.git&lt;/span&gt;
&lt;span class="nx"&gt;cd&lt;/span&gt; &lt;span class="nx"&gt;nextjs&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;semaphor&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;rls&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;demo&lt;/span&gt;
&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt;
&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="nx"&gt;dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the remainder of the post, we'll highlight the steps needed to enable row-level security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Define Row-Level Policy in Semaphor Console.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigate to your project in the Semaphor console. You can request a free trial &lt;a href="https://semaphor.cloud" rel="noopener noreferrer"&gt;here&lt;/a&gt;. You should see a "demo project" created for you when you first logged in. Under the "Security" tab, navigate to "Row/Column Level Security." Create a new policy as shown below. Make sure to allow all columns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.semaphor.cloud%2F_next%2Fimage%3Furl%3D%252Fimages%252Frow-level-policy.png%26w%3D1200%26q%3D75" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.semaphor.cloud%2F_next%2Fimage%3Furl%3D%252Fimages%252Frow-level-policy.png%26w%3D1200%26q%3D75" alt="Row-level Policy" width="800" height="666"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Generate AuthToken with this policy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Semaphor, the &lt;code&gt;AuthToken&lt;/code&gt; defines what a user can see on the dashboard. When rendering a dashboard for a specific tenant, you must include the row-level policy when you generate their &lt;code&gt;AuthToken&lt;/code&gt;. The row-level security policy acts like a "hard" filter, restricting user's view to only the rows permitted by the policy.&lt;/p&gt;

&lt;p&gt;For demonstration, we have used URL search parameters to determine which tenant's dashboard to render. In a production environment, you would likely use authentication logic to derive these parameters.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Tenant A: http://localhost:3000/?tenant=a&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Tenant B: http://localhost:3000/?tenant=b&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Tenant C: http://localhost:3000/?tenant=c&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// This is to disable the Nextjs caching behavior.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AuthToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;semaphor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DashboardComponent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/dashboard-component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;postRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/server-utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DASHBOARD_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;d_a1a5275b-5ded-411c-a2bb-bfafd55e7b26&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Replace with your actual dashboard ID&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DASHBOARD_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ds_13353da5-e413-46cb-a8af-fd58192aab11&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Replace with your actual dashboard secret&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOKEN_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://semaphor.cloud/api/v1/token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// extract tenant from search parameter&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenant&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// select the states&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;states&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New York&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connecticut&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Florida&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;states&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Texas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Illinois&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ohio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;states&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;California&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Nevada&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Washington&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;states&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// create a policy with the above states&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rowPolicy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;states&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// generate auth token with the policy&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;postRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AuthToken&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TOKEN_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dashboardId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DASHBOARD_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dashboardSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DASHBOARD_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rcls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rowPolicy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="c1"&gt;// only pass the row policy if there are multiple states&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DashboardComponent&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these simple modifications, you can enable multi-tenancy for your dashboard, allowing different clients or organizations to access their specific data through a single dashboard instance.&lt;/p&gt;

&lt;p&gt;This approach ensures data isolation and customized views for each tenant, enhancing both security and user experience.&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to get in touch with us at &lt;a href="mailto:support@semaphor.cloud"&gt;support@semaphor.cloud&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>multitenancy</category>
      <category>dashboards</category>
    </item>
    <item>
      <title>Fully-interactive analytics for your Next.js App</title>
      <dc:creator>Rohit Pujari</dc:creator>
      <pubDate>Thu, 19 Sep 2024 15:00:22 +0000</pubDate>
      <link>https://forem.com/rohitspujari/fully-interactive-analytics-for-your-nextjs-app-49dh</link>
      <guid>https://forem.com/rohitspujari/fully-interactive-analytics-for-your-nextjs-app-49dh</guid>
      <description>&lt;p&gt;If you're looking to launch fully-interactive analytics in your app but don't want to take on the scope of building it all in-house — which can be monumental (read &lt;a href="https://blog.semaphor.cloud/posts/the-right-way-to-approach-user-facing-analytics" rel="noopener noreferrer"&gt;here&lt;/a&gt; to learn why) — you've come to the right place.&lt;/p&gt;

&lt;p&gt;The fastest way to launch analytics in your product is by decoupling analytic content from your application code. Why? Because this separation doubles your shipping velocity. It allows you to modify analytics independently — adding new visuals or tweaking existing ones — without touching your product code. As a result, your dashboards can evolve without requiring you to rebuild and redeploy your app.&lt;/p&gt;

&lt;p&gt;This approach creates two parallel, non-blocking paths for executing your product roadmap. Dashboard development is a highly iterative process that can occur independently without involving your core product team. It also scales seamlessly as the scope of your product grows. Your product engineers can focus on developing core features, while customer success teams create personalized analytics experiences for clients. These complementary, non-blocking efforts allow you to ship faster and in a more modular way. Let's see it in action.&lt;/p&gt;

&lt;p&gt;For a quick start, you can simply clone this &lt;a href="https://github.com/rohitspujari/nextjs-semaphor-demo" rel="noopener noreferrer"&gt;Git repository&lt;/a&gt; and run it locally on your machine as shown below. This repository includes a demo dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;git clone https://github.com/rohitspujari/nextjs-semaphor-demo.git
cd nextjs-semaphor-demo
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The remainder of this blog post provides a detailed, step-by-step walkthrough of the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Request Semaphor free trial &lt;a href="https://semaphor.cloud/home" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you log in, you'll see a demo dashboard created for you. With Semaphor, you can connect to any SQL-compatible data source, including CSV files, Parquet files in Amazon S3, or responses from REST APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Create a dashboard using natural language, SQL, or Python.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Semaphor console allows you to customize the dashboard's appearance—including colors, fonts, corner styles, and layouts for various devices—giving you precise control over how it renders in your app. See a sneak peak here. &lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/BGOB6Ko_IFM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Integrate the dashboard into your Next JS app.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a new Next.js app. Skip this step if you already have an existing one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;npx create-next-app@latest

✔ What is your project named? nextjs-semaphor-demo
✔ Would you like to use TypeScript? Yes
✔ Would you like to use ESLint? Yes
✔ Would you like to use Tailwind CSS? Yes
✔ Would you like to use &lt;span class="sb"&gt;`src/`&lt;/span&gt; directory? Yes
✔ Would you like to use App Router? (recommended) Yes
✔ Would you like to customize the default import alias (@/&lt;span class="se"&gt;\*&lt;/span&gt;)? No

cd nextjs-semaphor-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the Semaphor package and start the app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;npm i semaphor@latest
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create a client component &lt;code&gt;dashboard-component.tsx&lt;/code&gt; as a container for your Semaphor dashboard. Note that the Semaphor dashboard is designed as a client component because it needs to support UI interactions such as drop-downs and on-click events.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/components/dashboard-component.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AuthToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;semaphor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;semaphor/style.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// IMPORTANT! Impport the CSS file. This is the default style, you can customize it.&lt;/span&gt;

&lt;span class="c1"&gt;// Dynamic import to prevent server-side rendering.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Dashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;semaphor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DashboardProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DashboardComponent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;authToken&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;DashboardProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dashboard&lt;/span&gt; &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;authToken&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt; &lt;span class="c1"&gt;// one line of code to render the dashboard&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;DashboardComponent&lt;/code&gt; accepts &lt;code&gt;AuthToken&lt;/code&gt; as a prop, authorizing the user to view the dashboard. This token-based approach allows dynamically display different dashboard depending on your app user or tenant.&lt;/p&gt;

&lt;p&gt;To generate the &lt;code&gt;AuthToken&lt;/code&gt; you need dashboard credentials. You can get your &lt;code&gt;dashboard id&lt;/code&gt; and &lt;code&gt;dashboard secret&lt;/code&gt; from the Semaphor console.&lt;/p&gt;

&lt;p&gt;IMPORTANT: You must use the server-side environment to request the token. This prevents the accidental exposure of the dashboard secret in client-side code. In Next.js, we can easily do that with a server-side function as shown below.&lt;/p&gt;

&lt;p&gt;This function makes a simple fetch call to the Seamphor endpoint to generate the token. The &lt;code&gt;use server&lt;/code&gt; notation ensures that this code only runs on the server.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/server-utils.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;postRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;errorResponse&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Request failed with status &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Network or processing error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're almost done. In your &lt;code&gt;page.tsx&lt;/code&gt; file, you can use the &lt;code&gt;postRequest&lt;/code&gt; server function as shown below.&lt;/p&gt;

&lt;p&gt;Note that we use &lt;code&gt;export const revalidate = 0&lt;/code&gt; at the top of the page to tell Next.js not to cache the token during build time. This is important because Semaphor tokens expire every 30 minutes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/app/page.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// This is to disable the Nextjs caching behavior.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AuthToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;semaphor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DashboardComponent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/dashboard-component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;postRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/server-utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DASHBOARD_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;d_a1a5275b-5ded-411c-a2bb-bfafd55e7b26&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Replace with your actual dashboard ID&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DASHBOARD_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ds_13353da5-e413-46cb-a8af-fd58192aab11&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Replace with your actual dashboard secret&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOKEN_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://semaphor.cloud/api/v1/token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// TODO: perform user authentication here&lt;/span&gt;

  &lt;span class="c1"&gt;// generate token for the user&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;postRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AuthToken&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TOKEN_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dashboardId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DASHBOARD_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dashboardSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DASHBOARD_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DashboardComponent&lt;/span&gt; &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Refresh your page and see your fully interactive dashboard, powered by Semaphor, come to life.&lt;/p&gt;

&lt;p&gt;In our &lt;a href="https://dev.to/rohitspujari/multi-tenant-analytics-for-nextjs-apps-3d1h"&gt;next blog post&lt;/a&gt;, we'll explore how you can add multi-tenancy and row-level security to your Semaphor dashboards.&lt;/p&gt;

&lt;p&gt;If you have any questions, feel free to drop us a note at &lt;a href="mailto:support@semaphor.cloud"&gt;support@semaphor.cloud&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>dashboards</category>
      <category>analytics</category>
    </item>
    <item>
      <title>The Right Way to Approach Customer-Facing Analytics</title>
      <dc:creator>Rohit Pujari</dc:creator>
      <pubDate>Thu, 15 Aug 2024 02:42:30 +0000</pubDate>
      <link>https://forem.com/rohitspujari/the-right-way-to-approach-customer-facing-analytics-3o41</link>
      <guid>https://forem.com/rohitspujari/the-right-way-to-approach-customer-facing-analytics-3o41</guid>
      <description>&lt;p&gt;Once you deliver on your core product promise, the next thing your customers will undoubtedly ask is better visibility and reporting around the business process your software enables. They want to track metrics, trends, and patterns around the “thing” your software helps them manage. At its core, almost every product we use helps us manage something we care about, whether it’s calls, schedules, shipments, meetings, jobs, interviews, people, [pick your domain].&lt;/p&gt;

&lt;p&gt;This need for insight is universally true for both internal and customer-facing apps. If you’re providing Saas, offering robust analytics right into your product can set you apart from the competition and even open up new revenue streams. But launching customer-facing analytics can lead you to false starts and wasted effort if you underestimate the following:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) The Analytics Scope Snowballs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What starts as a simple feature request often evolves into a substantial project — sometimes even larger than the core product itself. For startups and mid-sized companies, this can be a real problem. Every time a customer asks for a new visual or a tweak to a dashboard, it turns into a new product feature request (PFR) for your engineering team and requires a full code build, test, release cycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Analytics isn’t one size fits all&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From my consulting days, I’ve learned that every organization, team, or client wants a slightly different view of the same data. And they’re not wrong — users have their own goals to meet and have preferences on how they want to see their data, or narrow down to only what’s relevant to them. A read-only canned dashboard is hardly ever useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Security and Performance: Non-Negotiables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you externalize analytics, i.e, make your data accessible to users outside of your organization. you need to think about how to best isolate the data. With multiple users — and potentially multiple organizations — using your app, you’ll need to build a multi-tenant architecture that spans different analytical stores. If you are presenting data from object stores like S3, you need to have on-demand caching to maintain acceptable performance.&lt;/p&gt;

&lt;p&gt;A lot of companies think they can handle this in-house. After all, how hard can it be to add a few charts, right? But here’s the thing: when you give users a chart, they’ll want a dashboard. Give them a dashboard, and they’ll ask for filters. Before you know it, they’ll be asking for custom views, and then AaaayEyee (AI). Suddenly, you’re spending more time on analytics than on your actual product.&lt;/p&gt;

&lt;p&gt;One of our customers summed it up well:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Analytics is a necessary part of our product, but it shouldn’t come at the expense of slowing down our core roadmap.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Common Approach (And Why It Doesn’t Work)
&lt;/h2&gt;

&lt;p&gt;So, what do most companies do? They turn to their old-guard BI tools. The legacy BI vendors are more than happy to shrink wrap their whole product into an iframe and tell you to just drop it into your app. This approach works if you are embedding a dashboard in wikis or small internal apps. But if you’re building a customer-facing app, this approach can lead to some serious issues.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bloated Apps:&lt;/strong&gt; Those iframes come with a lot of unnecessary code, which can bloat your app. Why? because all those legacy BI tools were NEVER built to live inside of your apps. They were built as stand alone apps for internal BI needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A Frankenstein Experience:&lt;/strong&gt; Your app can end up looking like a disjointed mess, with limited styling options and a user experience that doesn’t respond well to different screen sizes. Every small customization seems like a band-aid fix, piling on more inconsistencies and leaving your app feeling more fragmented with each change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Opaque and Restrictive:&lt;/strong&gt; These tools often hold your data hostage, requiring proprietary tech to view analytics. You might not even be able to view the underlying business logic of the chart, let alone modify it. You will be forced to do things the BI way, and not the way your application expects. Plus, managing users in both the BI tool and your app can become a nightmare. So the next time you hear the term “Unified BI,” think “Bloated BI.”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Right Approach
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Decouple Analytics from Product Code:&lt;/strong&gt; This decoupling allows you to 2X your shipping velocity. When you separate your product codebase from analytics, you can move faster. Your engineering team can focus on the core features, while your customer success team can rapidly prototype and launch new experiences without touching the product code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Go for Open Standards:&lt;/strong&gt; Choose technologies where you can see and control the code behind your analytics. This transparency gives you the flexibility to deliver exactly what your customers need.&lt;br&gt;
Use &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use Framework-Specific Components:&lt;/strong&gt; Choose a tool that’s designed from the ground up to live inside of your app as a first class citizen. Use native components such as React, Vue, or Web Components. This approach lets you create a responsive, cohesive experience for your users.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stick to Modern Web Standards:&lt;/strong&gt; HTML, JavaScript, and CSS aren’t going anywhere. While nifty Python-based wrappers like streamlit are great for throwaway prototypes, when it’s time to ship, native web technologies perform better and are easier to maintain and customize. Do it the right way the first time. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep It Simple and Flexible:&lt;/strong&gt; Most BI tools are overly complex, requiring too many steps to do something simple. Less is sometimes more when it comes to customer-facing analytics. The analytics experience you provide must be intuitive and shouldn’t require training.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s a new wave of products — like Semaphor — that are taking this approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Semaphor
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://semaphor.cloud" rel="noopener noreferrer"&gt;&lt;strong&gt;Semaphor&lt;/strong&gt;&lt;/a&gt; is a fully customizable analytics package for delivering user-facing analytics in your apps.&lt;/p&gt;

&lt;p&gt;We took our inspiration from Stripe! Just like when you want to collect payments in your app, you don’t build a full-blown payments collection infrastructure from scratch. You use something like Stripe! With just a few lines of code, you can start collecting payments. This way, you can focus on what makes your product unique.&lt;/p&gt;

&lt;p&gt;We do the same for analytics. If you want to launch fully-responsive, AI-powered, interactive analytics in your app, you use Semaphor. With just a few lines of code, you can deliver branded analytics in your app. You can style almost everything to precisely match the look and feel of your app.&lt;/p&gt;

&lt;p&gt;Semaphor offers a canvas experience where users can view a set of visuals and engage with a dashboard conversationally. As they discover new insights, they can easily add them to the existing dashboard and personalize the experience. This is fundamentally different from the read-only dashboards provided by legacy BI vendors.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Ve27nq1vDoA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here’s what Semaphor brings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open Standards:&lt;/strong&gt; We understand SQL and Python and you get complete transparency and control over how every insight is generated. No lock-in, just flexible, powerful analytics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plug and Play:&lt;/strong&gt; Use framework-specific components like React, Vue, or Web Components to render insights directly into your product — no clunky iframes!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deep Customization:&lt;/strong&gt; Tailor every aspect of your dashboard — styles, fonts, colors — to match your brand. Bring your own visuals if you like and create layouts that look great on any device.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-Time Insights:&lt;/strong&gt; Deliver insights at the speed of your business — as fast as your data comes in, and wherever it comes from, whether it is databases, APIs, or other sources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dashboard-as-Code:&lt;/strong&gt; Manage your dashboards as code — version control and automate deployments just like any other software. Your dashboards always remain secure, version-controlled, and consistently backed up.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to learn more, set up a demo &lt;a href="https://cal.com/rohitspujari" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The fastest way to launch customer-facing analytics in your apps.</title>
      <dc:creator>Rohit Pujari</dc:creator>
      <pubDate>Mon, 29 Apr 2024 20:12:31 +0000</pubDate>
      <link>https://forem.com/rohitspujari/the-fastest-way-to-launch-customer-facing-analytics-in-your-apps-4noi</link>
      <guid>https://forem.com/rohitspujari/the-fastest-way-to-launch-customer-facing-analytics-in-your-apps-4noi</guid>
      <description>&lt;p&gt;You're cruising along, building your SaaS, and then BAM! Your customers hit you with a request for better visibility into the "thing" your software does. Whether it's processing payments, shipments, or refreshments, no matter what your business is, your customers want to know how they can get the most value out of your app.&lt;/p&gt;

&lt;p&gt;You might think, "No sweat, let's throw in some charts and call it a win." But hold onto your hats because your users are about to throw you a curveball. They see a chart, they want a dashboard. You give them a dashboard, they demand filters. Next, they're itching for some custom views. Before you know it, you're spending more time on the analytics than the actual product itself.&lt;/p&gt;

&lt;p&gt;That's where &lt;a href="https://semaphor.cloud/home" rel="noopener noreferrer"&gt;Semaphor&lt;/a&gt; comes to the rescue. It's a fully-customizable analytics package that lets you whip up interactive dashboards in your product in days, not months, and it's as simple as one line of code.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Ve27nq1vDoA"&gt;
&lt;/iframe&gt;
.&lt;/p&gt;
&lt;h2&gt;
  
  
  Show me the magic
&lt;/h2&gt;

&lt;p&gt;You're probably wondering, "Alright, how does this magic happen?" It's as easy as 1-2-3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1)&lt;/strong&gt; Hook up your data source: Whether it's any SQL database or your preferred API, we've got you covered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2)&lt;/strong&gt; Speak naturally or go old school with your own SQL. And get this, we even support SQL over API – mind officially blown! 🤯 From there, turn that SQL into a visual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3)&lt;/strong&gt; Once you've crafted your masterpiece of a dashboard, commit it to Github, publish it, and voila! With just one line of code, this dashboard is ready to be woven into your app. Tailor it with your CSS so it looks like it was born and raised in your app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div&amp;gt;
   &amp;lt;h2&amp;gt;My cool dashboard&amp;lt;/h2&amp;gt;
   &amp;lt;Dashboard authToken={authToken} id={DASHBOARD_ID} /&amp;gt;
&amp;lt;/div&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What once took months, maybe even longer, is now a matter of hours, thanks to Semaphor. But hold up! What about security? Fear not! We've got you covered with row and column level security to ensure sure John see's John's business and not Janes!&lt;/p&gt;

&lt;p&gt;We are inviting trailblazers and early adopters. If you think your app needs some analytics facelift, join our &lt;a href="https://semaphor.cloud/home" rel="noopener noreferrer"&gt;waitlist&lt;/a&gt; by requesting access, and don't forget to explain your use case. &lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
