<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Gyri Infotech | Tech Memoir]]></title><description><![CDATA[Thoughts, stories and ideas.]]></description><link>https://blog.gyri.tech/</link><image><url>https://blog.gyri.tech/favicon.png</url><title>Gyri Infotech | Tech Memoir</title><link>https://blog.gyri.tech/</link></image><generator>Ghost 5.82</generator><lastBuildDate>Sun, 28 Jun 2026 12:24:04 GMT</lastBuildDate><atom:link href="https://blog.gyri.tech/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[GitLab CI/CD Pipeline Documentation]]></title><description><![CDATA[<h2 id="overview">Overview</h2>
<p>This repository uses a multi-environment GitLab CI/CD pipeline for a <strong>Spring Boot Reactive (WebFlux)</strong> application.</p>
<p>The pipeline provides:</p>
<ul>
<li>Automated build and deployment</li>
<li>Parallel deployment to multiple test environments</li>
<li>Mattermost notifications with changelog support</li>
<li>Automatic handling of unavailable GitLab runners</li>
<li>Deployment status tracking</li>
<li>Production deployment authorization controls</li>
<li>Deployment failure</li></ul>]]></description><link>https://blog.gyri.tech/gitlab-ci-cd-pipeline-documentation/</link><guid isPermaLink="false">6a33bad5a59c17040f4ea172</guid><dc:creator><![CDATA[Kaustubh Kesarkar]]></dc:creator><pubDate>Thu, 18 Jun 2026 10:06:31 GMT</pubDate><media:content url="https://blog.gyri.tech/content/images/2026/06/gitlab-architecture.png" medium="image"/><content:encoded><![CDATA[<h2 id="overview">Overview</h2>
<img src="https://blog.gyri.tech/content/images/2026/06/gitlab-architecture.png" alt="GitLab CI/CD Pipeline Documentation"><p>This repository uses a multi-environment GitLab CI/CD pipeline for a <strong>Spring Boot Reactive (WebFlux)</strong> application.</p>
<p>The pipeline provides:</p>
<ul>
<li>Automated build and deployment</li>
<li>Parallel deployment to multiple test environments</li>
<li>Mattermost notifications with changelog support</li>
<li>Automatic handling of unavailable GitLab runners</li>
<li>Deployment status tracking</li>
<li>Production deployment authorization controls</li>
<li>Deployment failure notifications with job logs</li>
</ul>
<hr>
<h1 id="architecture">Architecture</h1>
<h2 id="test-environments">Test Environments</h2>
<table>
<thead>
<tr>
<th>Environment</th>
<th>Location</th>
<th>Runner Tag</th>
</tr>
</thead>
<tbody>
<tr>
<td>TH</td>
<td>Test Server Location 1</td>
<td><code>th-runner</code></td>
</tr>
<tr>
<td>KP</td>
<td>Test Server Location 2</td>
<td><code>kp-runner</code></td>
</tr>
</tbody>
</table>
<p>Both environments build and deploy independently.</p>
<p>Pipeline success requires <strong>at least one test deployment</strong> to complete successfully.</p>
<hr>
<h1 id="pipeline-stages">Pipeline Stages</h1>
<pre><code class="language-text">notify_start
    &#x2502;
    &#x25BC;
build_test
    &#x251C;&#x2500;&#x2500; build_test_th
    &#x251C;&#x2500;&#x2500; build_test_kp
    &#x2514;&#x2500;&#x2500; pending_job_monitor_build_test
    &#x2502;
    &#x25BC;
deploy_test
    &#x251C;&#x2500;&#x2500; deploy_test_th
    &#x251C;&#x2500;&#x2500; deploy_test_kp
    &#x2514;&#x2500;&#x2500; pending_job_monitor_deploy_test
    &#x2502;
    &#x25BC;
pipeline_complete
    &#x2502;
    &#x25BC;
deploy_prod (manual)
</code></pre>
<hr>
<h1 id="stage-details">Stage Details</h1>
<hr>
<h2 id="1-notifystart">1. notify_start</h2>
<h3 id="purpose">Purpose</h3>
<p>Sends a pipeline start notification to Mattermost.</p>
<h3 id="features">Features</h3>
<ul>
<li>Includes pipeline link</li>
<li>Includes changelog</li>
<li>Displays recent commits</li>
</ul>
<p>Example:</p>
<pre><code class="language-text">&#x1F680; Build STARTED

Changes in this build:

- abc123 Fixed login issue
- def456 Added Kafka consumer
- ghi789 Updated API validation
</code></pre>
<hr>
<h2 id="2-buildtest">2. build_test</h2>
<h3 id="jobs">Jobs</h3>
<h4 id="buildtestth">build_test_th</h4>
<p>Builds application for TH environment.</p>
<h4 id="buildtestkp">build_test_kp</h4>
<p>Builds application for KP environment.</p>
<h3 id="build-process">Build Process</h3>
<ol>
<li>Copy example property files</li>
<li>Generate environment-specific property file</li>
<li>Run Gradle build</li>
</ol>
<pre><code class="language-bash">./gradlew clean build \
-Pspring.config.additional-location=file:./application-test.properties
</code></pre>
<h3 id="generated-artifacts">Generated Artifacts</h3>
<pre><code class="language-text">build/libs/spring-boot-template-cqrs-1.0.0.jar
application-test.properties
</code></pre>
<p>Artifacts expire after:</p>
<pre><code class="language-text">1 day
</code></pre>
<hr>
<h2 id="3-runner-availability-monitoring">3. Runner Availability Monitoring</h2>
<h3 id="pendingjobmonitorbuildtest">pending_job_monitor_build_test</h3>
<p>Problem:</p>
<p>If a dedicated runner is unavailable, jobs can remain stuck in <code>Pending</code>.</p>
<p>Solution:</p>
<p>After 30 seconds:</p>
<ol>
<li>Wait configured timeout</li>
<li>Query GitLab API</li>
<li>Detect pending jobs</li>
<li>Cancel stuck jobs automatically</li>
</ol>
<p>Current timeout:</p>
<pre><code class="language-yaml">PENDING_JOB_TIMEOUT=15
</code></pre>
<hr>
<h2 id="4-deploytest">4. deploy_test</h2>
<h3 id="jobs">Jobs</h3>
<h4 id="deploytestth">deploy_test_th</h4>
<p>Deploys build artifact to TH environment.</p>
<h4 id="deploytestkp">deploy_test_kp</h4>
<p>Deploys build artifact to KP environment.</p>
<h3 id="deployment-flow">Deployment Flow</h3>
<h4 id="step-1">Step 1</h4>
<p>Backup existing JAR</p>
<pre><code class="language-bash">old.jar -&gt; old.jar_bkp
</code></pre>
<h4 id="step-2">Step 2</h4>
<p>Upload new JAR</p>
<pre><code class="language-bash">scp build/libs/app.jar
</code></pre>
<h4 id="step-3">Step 3</h4>
<p>Upload generated property file</p>
<pre><code class="language-bash">scp application.properties
</code></pre>
<h4 id="step-4">Step 4</h4>
<p>Execute remote deployment command</p>
<p>Example:</p>
<pre><code class="language-bash">docker compose restart
</code></pre>
<p>or</p>
<pre><code class="language-bash">systemctl restart my-service
</code></pre>
<hr>
<h2 id="deployment-validation">Deployment Validation</h2>
<p>Deployment only proceeds if:</p>
<pre><code class="language-text">Build succeeded
AND
Artifact exists
</code></pre>
<p>Otherwise deployment is skipped.</p>
<hr>
<h2 id="5-runner-availability-monitoring-deploy">5. Runner Availability Monitoring (Deploy)</h2>
<h3 id="pendingjobmonitordeploytest">pending_job_monitor_deploy_test</h3>
<p>Automatically cancels deploy jobs that remain pending due to unavailable runners.</p>
<hr>
<h2 id="6-pipelinecomplete">6. pipeline_complete</h2>
<h3 id="purpose">Purpose</h3>
<p>Determines final pipeline status.</p>
<h3 id="success-criteria">Success Criteria</h3>
<p>Pipeline succeeds if:</p>
<pre><code class="language-text">TH deployment succeeded
OR
KP deployment succeeded
</code></pre>
<h3 id="failure-criteria">Failure Criteria</h3>
<p>Pipeline fails if:</p>
<pre><code class="language-text">TH deployment failed
AND
KP deployment failed
</code></pre>
<p>Examples:</p>
<table>
<thead>
<tr>
<th>TH</th>
<th>KP</th>
<th>Pipeline</th>
</tr>
</thead>
<tbody>
<tr>
<td>Success</td>
<td>Success</td>
<td>Success</td>
</tr>
<tr>
<td>Success</td>
<td>Failed</td>
<td>Success</td>
</tr>
<tr>
<td>Failed</td>
<td>Success</td>
<td>Success</td>
</tr>
<tr>
<td>Failed</td>
<td>Failed</td>
<td>Failed</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="7-deployprod">7. deploy_prod</h2>
<h3 id="type">Type</h3>
<p>Manual deployment.</p>
<h3 id="authorization">Authorization</h3>
<p>Only approved GitLab users can deploy to production.</p>
<p>Validation:</p>
<pre><code class="language-bash">GITLAB_USERS_IDS_FOR_PROD_DEPLOY
</code></pre>
<p>contains:</p>
<pre><code class="language-bash">GITLAB_USER_ID
</code></pre>
<p>Unauthorized users receive:</p>
<pre><code class="language-text">&#x274C; User is not authorized for production deployment
</code></pre>
<h3 id="production-deployment-steps">Production Deployment Steps</h3>
<ol>
<li>Backup current JAR</li>
<li>Upload new JAR</li>
<li>Upload property file</li>
<li>Execute production deployment command</li>
<li>Send Mattermost notification</li>
</ol>
<hr>
<h1 id="mattermost-notifications">Mattermost Notifications</h1>
<hr>
<h2 id="start-notification">Start Notification</h2>
<p>Color:</p>
<pre><code class="language-text">Blue
</code></pre>
<p>Example:</p>
<pre><code class="language-text">&#x1F680; Build STARTED
</code></pre>
<hr>
<h2 id="deployment-success">Deployment Success</h2>
<p>Color:</p>
<pre><code class="language-text">Purple
</code></pre>
<p>Example:</p>
<pre><code class="language-text">&#x2705; TH Deploy SUCCESS
</code></pre>
<hr>
<h2 id="pipeline-success">Pipeline Success</h2>
<p>Color:</p>
<pre><code class="language-text">Green
</code></pre>
<p>Example:</p>
<pre><code class="language-text">&#x1F389; Pipeline SUCCESS
</code></pre>
<hr>
<h2 id="failure-notification">Failure Notification</h2>
<p>Color:</p>
<pre><code class="language-text">Red
</code></pre>
<p>Includes:</p>
<ul>
<li>Error details</li>
<li>Last 50 log lines</li>
<li>Pipeline link</li>
<li>Job URL</li>
</ul>
<p>Example:</p>
<pre><code class="language-text">&#x274C; Deploy FAILED

Job Log:
...
</code></pre>
<hr>
<h1 id="changelog-generation">Changelog Generation</h1>
<p>Each pipeline automatically generates a changelog.</p>
<p>Information included:</p>
<ul>
<li>Commit hash</li>
<li>Author</li>
<li>Commit message</li>
</ul>
<p>Example:</p>
<pre><code class="language-text">- abc123 Added Kafka consumer [John]
- def456 Fixed Redis configuration [Mike]
</code></pre>
<p>Limits:</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>MAX_COMMIT_MSG_LENGTH</td>
<td>100</td>
</tr>
<tr>
<td>MAX_COMMITS_TO_SHOW</td>
<td>10</td>
</tr>
</tbody>
</table>
<hr>
<h1 id="required-gitlab-variables">Required GitLab Variables</h1>
<h2 id="general">General</h2>
<pre><code class="language-text">APP_NAME

GYRI_TEST_PROPERTY_FILE

GYRI_TEST_MATTERMOST_WEBHOOK
ALL_FAIL_MATTERMOST_WEBHOOK

GITLAB_API_TOKEN
</code></pre>
<hr>
<h2 id="th-environment">TH Environment</h2>
<pre><code class="language-text">GYRI_TEST_TH_SERVER_USER
GYRI_TEST_TH_SERVER_01
GYRI_TEST_TH_SERVER_01_DEPLOYMENT_PATH

TEST_TH_DEPLOY_COMMAND

TEST_TH_APP_LOG_FILE_PROPERTY
TEST_TH_APP_BASE_SERVER_URL
TEST_TH_SWAGGER_SERVER_URL

TEST_TH_REDIS_HOST
TEST_TH_REDIS_PORT
TEST_TH_REDIS_TOPIC_PREFIX

TEST_TH_MONGO_HOST
TEST_TH_MONGO_PORT
TEST_TH_MONGO_DATABASE
TEST_TH_MONGO_USERNAME
TEST_TH_MONGO_PASSWORD
TEST_TH_MONGO_AUTH_DATABASE

TEST_TH_KAFKA_SECURITY_PROTOCOL
TEST_TH_KAFKA_SASL_MECHANISM
TEST_TH_KAFKA_SASL_JAAS_CONFIG
TEST_TH_KAFKA_SERVERS
TEST_TH_KAFKA_CONSUMER_GROUP_ID
TEST_TH_KAFKA_TOPIC_PREFIX
</code></pre>
<hr>
<h2 id="kp-environment">KP Environment</h2>
<pre><code class="language-text">GYRI_TEST_KP_SERVER_USER
GYRI_TEST_KP_SERVER_01
GYRI_TEST_KP_SERVER_01_DEPLOYMENT_PATH

TEST_KP_DEPLOY_COMMAND

TEST_KP_APP_LOG_FILE_PROPERTY
TEST_KP_APP_BASE_SERVER_URL
TEST_KP_SWAGGER_SERVER_URL

TEST_KP_REDIS_HOST
TEST_KP_REDIS_PORT
TEST_KP_REDIS_TOPIC_PREFIX

TEST_KP_MONGO_HOST
TEST_KP_MONGO_PORT
TEST_KP_MONGO_DATABASE
TEST_KP_MONGO_USERNAME
TEST_KP_MONGO_PASSWORD
TEST_KP_MONGO_AUTH_DATABASE

TEST_KP_KAFKA_SECURITY_PROTOCOL
TEST_KP_KAFKA_SASL_MECHANISM
TEST_KP_KAFKA_SASL_JAAS_CONFIG
TEST_KP_KAFKA_SERVERS
TEST_KP_KAFKA_CONSUMER_GROUP_ID
TEST_KP_KAFKA_TOPIC_PREFIX
</code></pre>
<hr>
<h2 id="production">Production</h2>
<pre><code class="language-text">GYRI_PROD_SERVER_USER
GYRI_PROD_SERVER_HOST
GYRI_PROD_SERVER_SSH_PORT
GYRI_PROD_SERVER_DEPLOYMENT_PATH

PROD_DEPLOY_COMMAND

GYRI_PROD_MATTERMOST_WEBHOOK

GITLAB_USERS_IDS_FOR_PROD_DEPLOY
</code></pre>
<hr>
<h1 id="gitlab-runner-registration">GitLab Runner Registration</h1>
<p>The pipeline requires three runners:</p>
<table>
<thead>
<tr>
<th>Runner</th>
<th>Tag</th>
</tr>
</thead>
<tbody>
<tr>
<td>TH Runner</td>
<td><code>th-runner</code></td>
</tr>
<tr>
<td>KP Runner</td>
<td><code>kp-runner</code></td>
</tr>
<tr>
<td>Production Runner</td>
<td><code>prod-runner</code></td>
</tr>
</tbody>
</table>
<hr>
<h2 id="install-gitlab-runner">Install GitLab Runner</h2>
<h3 id="ubuntudebian">Ubuntu/Debian</h3>
<pre><code class="language-bash">curl -L \
https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh \
| sudo bash

sudo apt install gitlab-runner
</code></pre>
<p>Verify:</p>
<pre><code class="language-bash">gitlab-runner --version
</code></pre>
<hr>
<h2 id="register-th-runner">Register TH Runner</h2>
<pre><code class="language-bash">sudo gitlab-runner register
</code></pre>
<p>Prompts:</p>
<pre><code class="language-text">GitLab URL:
https://gitlab.example.com

Registration Token:
&lt;project-runner-token&gt;

Description:
th-runner

Tags:
th-runner

Executor:
shell
</code></pre>
<hr>
<h2 id="register-kp-runner">Register KP Runner</h2>
<pre><code class="language-bash">sudo gitlab-runner register
</code></pre>
<p>Prompts:</p>
<pre><code class="language-text">GitLab URL:
https://gitlab.example.com

Registration Token:
&lt;project-runner-token&gt;

Description:
kp-runner

Tags:
kp-runner

Executor:
shell
</code></pre>
<hr>
<h2 id="register-production-runner">Register Production Runner</h2>
<pre><code class="language-bash">sudo gitlab-runner register
</code></pre>
<p>Prompts:</p>
<pre><code class="language-text">GitLab URL:
https://gitlab.example.com

Registration Token:
&lt;project-runner-token&gt;

Description:
prod-runner

Tags:
prod-runner

Executor:
shell
</code></pre>
<hr>
<h2 id="verify-registered-runners">Verify Registered Runners</h2>
<pre><code class="language-bash">sudo gitlab-runner list
</code></pre>
<p>Example:</p>
<pre><code class="language-text">th-runner
kp-runner
prod-runner
</code></pre>
<hr>
<h2 id="start-runner-service">Start Runner Service</h2>
<pre><code class="language-bash">sudo systemctl enable gitlab-runner
sudo systemctl start gitlab-runner
</code></pre>
<p>Check status:</p>
<pre><code class="language-bash">sudo systemctl status gitlab-runner
</code></pre>
<hr>
<h1 id="deployment-directory-structure">Deployment Directory Structure</h1>
<p>Expected structure on target servers:</p>
<pre><code class="language-text">template-spring-cqrs-reactjs/

&#x251C;&#x2500;&#x2500; server/
&#x2502;
&#x251C;&#x2500;&#x2500; artifacts/
&#x2502;   &#x251C;&#x2500;&#x2500; application.jar
&#x2502;   &#x2514;&#x2500;&#x2500; application.jar_bkp
&#x2502;
&#x2514;&#x2500;&#x2500; server-config/
    &#x2514;&#x2500;&#x2500; application.properties
</code></pre>
<hr>
<h1 id="failure-handling">Failure Handling</h1>
<p>The pipeline is designed to tolerate infrastructure failures.</p>
<p>Examples:</p>
<h3 id="th-runner-offline">TH Runner Offline</h3>
<pre><code class="language-text">TH Build &#x2192; Failed

KP Build &#x2192; Success

KP Deploy &#x2192; Success

Pipeline &#x2192; SUCCESS
</code></pre>
<h3 id="kp-runner-offline">KP Runner Offline</h3>
<pre><code class="language-text">TH Build &#x2192; Success

TH Deploy &#x2192; Success

Pipeline &#x2192; SUCCESS
</code></pre>
<h3 id="both-runners-offline">Both Runners Offline</h3>
<pre><code class="language-text">TH Build &#x2192; Failed

KP Build &#x2192; Failed

Pipeline &#x2192; FAILED
</code></pre>
<hr>
<h1 id="summary">Summary</h1>
<p>This pipeline provides:</p>
<ul>
<li>Multi-location deployment (TH + KP)</li>
<li>Spring Boot Reactive build automation</li>
<li>Automatic changelog generation</li>
<li>Mattermost notifications</li>
<li>Job log reporting on failures</li>
<li>Runner health monitoring</li>
<li>Automatic cancellation of stuck jobs</li>
<li>Production deployment authorization</li>
<li>Artifact backup and rollback preparation</li>
<li>High availability deployment strategy</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[React Redux v9 - `connect`]]></title><description><![CDATA[<h1 id="react-redux-v9-why-connect-shows-a-deprecated-warning-and-how-to-fix-it">React Redux v9: Why <code>connect()</code> Shows a Deprecated Warning and How to Fix It</h1>
<p>If you&apos;ve recently upgraded to React Redux v9 and are using IntelliJ, VS Code, or another TypeScript-aware IDE, you may have noticed a warning when hovering over <code>connect()</code>:</p>
<pre><code class="language-ts">@deprecated
We recommend using the useSelector</code></pre>]]></description><link>https://blog.gyri.tech/react-redux-v9-connect/</link><guid isPermaLink="false">6a30edc45d149203fdd917c1</guid><dc:creator><![CDATA[Kaustubh Kesarkar]]></dc:creator><pubDate>Tue, 16 Jun 2026 06:36:58 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1573496773905-f5b17e717f05?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDg0fHx0ZWNoJTIwbWlncmF0aW9ufGVufDB8fHx8MTc4MTU5MTY1Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<h1 id="react-redux-v9-why-connect-shows-a-deprecated-warning-and-how-to-fix-it">React Redux v9: Why <code>connect()</code> Shows a Deprecated Warning and How to Fix It</h1>
<img src="https://images.unsplash.com/photo-1573496773905-f5b17e717f05?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDg0fHx0ZWNoJTIwbWlncmF0aW9ufGVufDB8fHx8MTc4MTU5MTY1Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="React Redux v9 - `connect`"><p>If you&apos;ve recently upgraded to React Redux v9 and are using IntelliJ, VS Code, or another TypeScript-aware IDE, you may have noticed a warning when hovering over <code>connect()</code>:</p>
<pre><code class="language-ts">@deprecated
We recommend using the useSelector and useDispatch hooks instead.
See https://react-redux.js.org/api/hooks

If you need to use connect without this visual deprecation warning,
import legacy_connect instead:

import { legacy_connect as connect } from &apos;react-redux&apos;
</code></pre>
<p>This can be surprising, especially because many existing React applications still use <code>connect()</code> extensively.</p>
<p>Let&apos;s understand what&apos;s happening and explore the available options.</p>
<h2 id="is-connect-actually-deprecated">Is <code>connect()</code> Actually Deprecated?</h2>
<p>Not exactly.</p>
<p>React Redux v9 adds a TypeScript/JSDoc deprecation annotation to encourage developers to adopt the modern Hooks API:</p>
<ul>
<li><code>useSelector()</code></li>
<li><code>useDispatch()</code></li>
</ul>
<p>However, <code>connect()</code> still works and remains supported.</p>
<p>The warning is primarily intended to guide developers toward the recommended pattern for function components.</p>
<h2 id="example-existing-connect-implementation">Example: Existing <code>connect()</code> Implementation</h2>
<p>Many applications use a pattern similar to this:</p>
<pre><code class="language-tsx">import { useNavigate } from &apos;react-router-dom&apos;;
import { useEffect } from &apos;react&apos;;
import { connect } from &apos;react-redux&apos;;

const ForgotPassword = ({ auth }) =&gt; {
  const navigate = useNavigate();

  useEffect(() =&gt; {
    if (auth.loggedIn) {
      navigate(&apos;/dashboard&apos;);
    }
  }, [auth.loggedIn, navigate]);

  return &lt;div&gt;Forgot Password&lt;/div&gt;;
};

const mapState = (state) =&gt; ({
  auth: state.auth,
});

export default connect(mapState)(ForgotPassword);
</code></pre>
<p>This code is completely valid and will continue to work.</p>
<p>The only issue is the IDE warning.</p>
<hr>
<h2 id="option-1-migrate-to-hooks-recommended">Option 1: Migrate to Hooks (Recommended)</h2>
<p>For function components, React Redux recommends using hooks instead of <code>connect()</code>.</p>
<h3 id="before">Before</h3>
<pre><code class="language-tsx">export default connect(mapState)(ForgotPassword);
</code></pre>
<h3 id="after">After</h3>
<pre><code class="language-tsx">import { useSelector } from &apos;react-redux&apos;;

const ForgotPassword = () =&gt; {
  const auth = useSelector((state) =&gt; state.auth);

  return &lt;div&gt;Forgot Password&lt;/div&gt;;
};

export default ForgotPassword;
</code></pre>
<p>A more complete example:</p>
<pre><code class="language-tsx">import { useNavigate } from &apos;react-router-dom&apos;;
import { useEffect } from &apos;react&apos;;
import { useSelector } from &apos;react-redux&apos;;

const ForgotPassword = () =&gt; {
  const navigate = useNavigate();

  const auth = useSelector((state) =&gt; state.auth);

  useEffect(() =&gt; {
    if (auth.loggedIn) {
      navigate(&apos;/dashboard&apos;);
    }
  }, [auth.loggedIn, navigate]);

  return &lt;div&gt;Forgot Password&lt;/div&gt;;
};

export default ForgotPassword;
</code></pre>
<h3 id="benefits">Benefits</h3>
<ul>
<li>Less boilerplate</li>
<li>No <code>mapStateToProps</code></li>
<li>Better TypeScript support</li>
<li>Officially recommended by React Redux</li>
</ul>
<hr>
<h2 id="option-2-continue-using-connect">Option 2: Continue Using <code>connect()</code></h2>
<p>If your application already contains many connected components, there is no immediate need to refactor everything.</p>
<p>You can simply keep using:</p>
<pre><code class="language-tsx">import { connect } from &apos;react-redux&apos;;
</code></pre>
<p>The warning is informational and does not affect runtime behavior.</p>
<p>This approach is often preferred in mature enterprise applications where consistency is more important than adopting the latest syntax.</p>
<hr>
<h2 id="option-3-use-legacyconnect">Option 3: Use <code>legacy_connect</code></h2>
<p>React Redux v9 introduces a special alias called <code>legacy_connect</code>.</p>
<p>Replace:</p>
<pre><code class="language-tsx">import { connect } from &apos;react-redux&apos;;
</code></pre>
<p>with:</p>
<pre><code class="language-tsx">import { legacy_connect as connect } from &apos;react-redux&apos;;
</code></pre>
<p>The rest of your code remains unchanged:</p>
<pre><code class="language-tsx">const mapState = (state) =&gt; ({
  auth: state.auth,
});

export default connect(mapState)(ForgotPassword);
</code></pre>
<h3 id="why-use-legacyconnect">Why Use <code>legacy_connect</code>?</h3>
<ul>
<li>Removes IDE deprecation warnings</li>
<li>No behavior changes</li>
<li>No refactoring required</li>
<li>Useful for large existing codebases</li>
</ul>
<hr>
<h2 id="recommended-approach-for-different-scenarios">Recommended Approach for Different Scenarios</h2>
<h3 id="new-projects">New Projects</h3>
<p>Use hooks:</p>
<pre><code class="language-tsx">useSelector()
useDispatch()
</code></pre>
<p>This is the modern React Redux pattern.</p>
<h3 id="existing-applications">Existing Applications</h3>
<p>If your project already contains dozens or hundreds of connected components:</p>
<pre><code class="language-tsx">import { legacy_connect as connect } from &apos;react-redux&apos;;
</code></pre>
<p>can be a practical intermediate solution.</p>
<h3 id="gradual-migration-strategy">Gradual Migration Strategy</h3>
<p>Many teams adopt the following approach:</p>
<ol>
<li>Keep existing <code>connect()</code> components.</li>
<li>Use hooks for all new components.</li>
<li>Gradually migrate old components when they are modified.</li>
<li>Use <code>legacy_connect</code> to suppress warnings during the transition.</li>
</ol>
<p>This avoids large-scale refactoring while moving toward modern React Redux patterns.</p>
<hr>
<h2 id="typescript-bonus-typed-hooks">TypeScript Bonus: Typed Hooks</h2>
<p>For Redux Toolkit applications, consider creating typed hooks.</p>
<pre><code class="language-ts">// store.ts
export type RootState = ReturnType&lt;typeof store.getState&gt;;
export type AppDispatch = typeof store.dispatch;
</code></pre>
<pre><code class="language-ts">// hooks.ts
import { useDispatch, useSelector } from &apos;react-redux&apos;;

export const useAppDispatch =
  useDispatch.withTypes&lt;AppDispatch&gt;();

export const useAppSelector =
  useSelector.withTypes&lt;RootState&gt;();
</code></pre>
<p>Usage:</p>
<pre><code class="language-tsx">const auth = useAppSelector((state) =&gt; state.auth);
</code></pre>
<p>This provides stronger type safety and a better developer experience.</p>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>The <code>connect()</code> warning in React Redux v9 is not an indication that the API has been removed. It is a recommendation from the React Redux team to use the Hooks API in modern React applications.</p>
<p>You can choose one of three paths:</p>
<ul>
<li><strong>Best for new code:</strong> <code>useSelector()</code> and <code>useDispatch()</code></li>
<li><strong>Best for existing code:</strong> continue using <code>connect()</code></li>
<li><strong>Best for removing warnings without refactoring:</strong> <code>legacy_connect</code></li>
</ul>
<p>For most teams, a gradual migration strategy offers the best balance between modernizing the codebase and minimizing risk.</p>
]]></content:encoded></item><item><title><![CDATA[Installing Zsh on Ubuntu: A Beginner-Friendly Guide]]></title><description><![CDATA[<p></p><p><strong>Introduction</strong></p><p>After installing Ubuntu, one of the first things many users encounter is the Linux terminal. By default,Ubuntu uses <strong>Bash (Bourne Again Shell)</strong> as its command-line shell. While Bash is powerful and widely used,many developers and Linux enthusiasts prefer an alternative shell called <strong>Zsh (Z Shell)</strong>.<br>This guide</p>]]></description><link>https://blog.gyri.tech/installing-zsh-on-ubuntu-a-beginner-friendly-guide/</link><guid isPermaLink="false">6a20116b5600f10407326b07</guid><dc:creator><![CDATA[Niranjan Kolwankar]]></dc:creator><pubDate>Thu, 11 Jun 2026 10:55:52 GMT</pubDate><media:content url="https://blog.gyri.tech/content/images/2026/06/zsh-7172334_640-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://blog.gyri.tech/content/images/2026/06/zsh-7172334_640-1.png" alt="Installing Zsh on Ubuntu: A Beginner-Friendly Guide"><p></p><p><strong>Introduction</strong></p><p>After installing Ubuntu, one of the first things many users encounter is the Linux terminal. By default,Ubuntu uses <strong>Bash (Bourne Again Shell)</strong> as its command-line shell. While Bash is powerful and widely used,many developers and Linux enthusiasts prefer an alternative shell called <strong>Zsh (Z Shell)</strong>.<br>This guide explains what Zsh is, why it is popular, how to install it on Ubuntu, and what happens during its<br>first-time setup. The explanations are written with beginners in mind, breaking down every command used<br>during the installation process.</p><hr><p><strong>What is a Shell?</strong></p><p>Before understanding Zsh, it is important to understand what a shell is.</p><p>A shell is a program that allows users to interact with the operating system through commands. Whenever commands such as the following are executed:</p><p><strong><em>ls<br>cd<br>mkdir</em></strong></p><p>the shell interprets those commands and communicates with the operating system to perform the requested actions.</p><p>Ubuntu uses <strong>Bash (Bourne Again Shell)</strong> as its default shell.</p><hr><p><strong>What is Zsh?</strong></p><p>Zsh stands for Z Shell. It is an advanced shell that provides all the features of Bash while adding many enhancements that improve the command-line experience.</p><p>Some of the most popular features of Zsh include:<br>        &#x2022; Better command auto-completion<br>        &#x2022; Easier navigation through directories<br>        &#x2022; Command suggestions and corrections<br>        &#x2022; Customizable themes<br>        &#x2022; Plugin support<br>        &#x2022; Improved productivity for developers<br></p><p>Because of these features, Zsh has become one of the most widely used shells in the Linux community.</p><hr><p><strong>Why Use Zsh?</strong></p><p>Many users choose Zsh for the following reasons:<br><strong><em>Better Auto-Completion</em></strong><br>Zsh provides intelligent suggestions when typing commands, file names, and directories.<br><strong><em>Improved Productivity</em></strong><br>Frequently used commands can be executed more quickly with the help of plugins and shortcuts.<br><strong><em>Customization</em></strong><br>The terminal appearance can be customized using themes, making it more informative and visually appealing.<br><strong><em>Learning Modern Linux Tools</em></strong><br>Many developers use Zsh along with frameworks such as Oh My Zsh, making it a valuable tool for anyone learning Linux development environments.</p><hr><p><strong>Installing Zsh</strong></p><p><em><strong>Step 1: Updating Package Information</strong></em><br>Before installing any software, Ubuntu&apos;s package information should be updated.<br><em>sudo apt update<br><strong>Understanding the Command</strong></em><br>   &#x2022; <em>sudo</em> = Super User DO. Temporarily grants administrator privileges.<br>   &#x2022; <em>apt</em> = Advanced Package Tool used to manage software packages.<br>   &#x2022; <em>update</em> = Downloads the latest package information from Ubuntu repositories.<br>This command does <strong>not upgrade software</strong>. It only refreshes the package database so Ubuntu knows which software versions are available.<br><em><strong>Step 2: Installing Zsh</strong></em><br>Install Zsh using the following command:<br><em>sudo apt install zsh -y<br><strong>Understanding the Command</strong></em><br>   &#x2022; <em>sudo</em> = Runs the command with administrator privileges.<br>   &#x2022; <em>apt</em> = Ubuntu package manager.<br>   &#x2022; <em>install</em> = Installs a software package.<br>   &#x2022; <em>zsh</em> = The package to be installed.<br>   &#x2022; -<em>y</em> = Automatically answers &quot;Yes&quot; to confirmation prompts.<br>This command downloads and installs Zsh along with any required dependencies.<br><em><strong>Step 3: Verifying Installation</strong></em><br>After installation, verify that Zsh has been installed correctly.<br><em>zsh --version<br><strong>Understanding the Command</strong></em><br>   &#x2022; <em>zsh</em> = Runs the Zsh executable.<br>   &#x2022; --<em>version</em> = Displays version information.<br>Example output:<br><em>zsh 5.9</em><br>Displaying a version number confirms that the installation was successful.<br><em><strong>Step 4: Making Zsh the Default Shell</strong></em><br>Installing Zsh does not automatically replace Bash. To make Zsh the default shell, execute:<br><em>chsh -s $(which zsh)<br><strong>Understanding the Command</strong></em><br>This command consists of multiple parts.<br><em>which zsh</em><br>   &#x2022; <em>which</em> = Finds the location of an executable program.<br>   &#x2022; <em>zsh</em> = The program being searched for.<br><em><strong>Example output:</strong><br>/usr/bin/zsh</em><br>This shows the location of the Zsh executable.<br><em>$(which zsh)</em><br>The $() syntax is known as <strong>command substitution</strong>.<br>It executes the command inside the brackets and uses its output.<br>Therefore:<br><em>$(which zsh)</em><br>becomes:<br><em>/usr/bin/zsh<br>chsh -s</em><br>   &#x2022; <em>chsh</em> = Change Shell.<br>   &#x2022; -<em>s</em> = Specifies which shell should be used.<br>Therefore:<br><em>chsh -s $(which zsh)</em><br>effectively becomes:<br><em>chsh -s /usr/bin/zsh</em><br>This tells Linux to use Zsh as the default login shell for the current user.<br>After running the command, log out and log back in.</p><hr><p><strong>First Launch of Zsh</strong></p><p>After logging back in and opening the terminal, Zsh may display a configuration screen called <strong>zsh-newuser-install.</strong><br>This happens because no Zsh configuration files currently exist in the user&apos;s home directory. Zsh therefore launches a setup wizard to create an initial configuration.<br>The setup wizard presents four options.<br><strong><em>Option (q)</em></strong><br>  (q) Quit and do nothing.<br>        &#x2022; Exits the setup wizard.<br>        &#x2022; The wizard will appear again the next time Zsh starts.<br><strong><em>Option (0)</em></strong><br>  (0) Exit, creating the file ~/.zshrc containing just a comment.<br>        &#x2022; Creates a minimal .zshrc file.<br>        &#x2022; Prevents the setup wizard from appearing again.<br>        &#x2022; Useful for users who want to configure everything manually.<br><strong><em>Option (1)</em></strong><br>  (1) Continue to the main menu.<br>        &#x2022; Opens the advanced configuration menu.<br>        &#x2022; Allows customization of history settings, key bindings, completion settings, and                                 other shell features. <br><strong><em>Option (2)</em></strong><br>  (2) Populate your ~/.zshrc with the configuration recommended by the system<br>  administrator.<br>        &#x2022; Automatically creates a recommended .zshrc file.<br>        &#x2022; Applies sensible default settings.<br>        &#x2022; Recommended for beginners.</p><hr><p><strong>Recommended Choice for Beginners</strong><br>For users who are new to Zsh, selecting:<br><strong>2</strong><br>is generally the best option.<br>This option:<br>        &#x2022; Creates the configuration file automatically.<br>        &#x2022; Applies useful default settings.<br>        &#x2022; Allows immediate use of Zsh without manual configuration.</p><hr><p><strong>Understanding the .zshrc File</strong><br>During the setup process, a file named:<br><em>~/.zshrc</em><br>is created.<br><strong><em>Breaking It Down</em></strong><br>   &#x2022; ~ = Home directory of the current user.<br>   &#x2022; .zshrc = Zsh Run Commands file.<br>   &#x2022; The dot ( . ) indicates that it is a hidden file.<br>The .zshrc file stores:<br>        &#x2022; Shell settings<br>        &#x2022; Aliases<br>        &#x2022; Plugins<br>        &#x2022; Themes<br>        &#x2022; Environment variables<br>        &#x2022; Custom terminal configurations<br>Every time Zsh starts, it reads this file and applies the stored settings.</p><hr><p><strong>Verifying the Configuration File</strong><br>To verify that the configuration file was created successfully:<br><em>ls -la ~/.zshrc<br><strong>Understanding the Command</strong></em><br>   &#x2022; ls = Lists files and directories.<br>   &#x2022; -l = Displays detailed information.<br>   &#x2022; -a = Shows hidden files.<br>   &#x2022; ~/.zshrc = Path of the configuration file.<br>If the file exists, Linux displays its details.</p><hr><p><strong>Key Takeaways</strong><br>Installing Zsh introduces several important Linux concepts:<br>        &#x2022; Linux supports multiple shells.<br>        &#x2022; Users can choose their preferred shell.<br>        &#x2022; Shell behavior is controlled through configuration files.<br>        &#x2022; Commands can be combined using command substitution.<br>        &#x2022; Terminal environments can be customized extensively.<br>Understanding these concepts provides a strong foundation for further Linux learning.</p><hr><p><strong>Conclusion</strong><br>Zsh is a modern and feature-rich shell that enhances the Linux terminal experience through improved auto-completion, customization, and productivity features. Installing Zsh is a simple process, but it also serves as an excellent opportunity to learn about package management, shell configuration, and Linux command-line fundamentals.<br>For beginners exploring Ubuntu and Linux, Zsh provides a practical introduction to terminal customization while maintaining compatibility with familiar Bash workflows.</p>]]></content:encoded></item><item><title><![CDATA[What is Kafka? (Part 1)]]></title><description><![CDATA[<p><u><strong>Definition:</strong></u> <br>Kafka, in simple terms, is a distributed-infrastructure used to store, manage and process <em>events</em> in real-time.<br><br>In traditional software world, developers visualized data as tables which represents things in real world like for example, items in an inventory, signed-up users, cars in a showroom, etc.<br>But what about things</p>]]></description><link>https://blog.gyri.tech/kafka-architecture-part-1/</link><guid isPermaLink="false">6a1d47925600f104073269c7</guid><dc:creator><![CDATA[Abhishek Bhingarde]]></dc:creator><pubDate>Mon, 01 Jun 2026 11:31:26 GMT</pubDate><media:content url="https://blog.gyri.tech/content/images/2026/06/kafka.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.gyri.tech/content/images/2026/06/kafka.jpg" alt="What is Kafka? (Part 1)"><p><u><strong>Definition:</strong></u> <br>Kafka, in simple terms, is a distributed-infrastructure used to store, manage and process <em>events</em> in real-time.<br><br>In traditional software world, developers visualized data as tables which represents things in real world like for example, items in an inventory, signed-up users, cars in a showroom, etc.<br>But what about things like item being dispatched from storeroom, user clicking somewhere on your website, driver picking up the client from a particular location?<br>You guessed it right! These are all <em>events</em>.<br>So, Kafka helps us process these events in real time, meaning the <u>moment that particular event took place.</u><br>Hence, there is no such concept of storing these events in a file or a table for processing them in a batch in future.<br>However, this does not goes on to conclude that Kafka cannot remember events that already took place. Kafka is built to remember these events. </p><p>Lets understand the need for Kafka with the help of this simple example.</p><p>Consider a thermostat_readings table.</p><table>
<thead>
<tr>
<th>sensor_id</th>
<th>location</th>
<th>temperature</th>
<th>read_at</th>
</tr>
</thead>
<tbody>
<tr>
<td>42</td>
<td>Mumbai</td>
<td>24</td>
<td>1700</td>
</tr>
<tr>
<td>51</td>
<td>Delhi</td>
<td>22</td>
<td>1715</td>
</tr>
<tr>
<td>76</td>
<td>Calicut</td>
<td>25</td>
<td>1600</td>
</tr>
</tbody>
</table>
<p>Now, suppose we need to update the thermostat reading for sensor_id 42 from 24 to 20, we will destructively change the value in temperature column for sensor id 42 from 24 to 20.<br>
So our new table, looks like:</p>
<table>
<thead>
<tr>
<th>sensor_id</th>
<th>location</th>
<th>temperature</th>
<th>read_at</th>
</tr>
</thead>
<tbody>
<tr>
<td>42</td>
<td>Mumbai</td>
<td>20</td>
<td>1700</td>
</tr>
<tr>
<td>51</td>
<td>Delhi</td>
<td>22</td>
<td>1715</td>
</tr>
<tr>
<td>76</td>
<td>Calicut</td>
<td>25</td>
<td>1600</td>
</tr>
</tbody>
</table>
<p>The critical issue here is that, we lost our previous info (context) of the sensor_id in Mumbai, like how quickly the temperature shoots up here or at what time of day it heats up. <br><br>To overcome this crucial challenge, Kafka implements <strong><em>logs.</em></strong></p><p>Now you might be wondering what are logs?<br>Logs can be interpreted as an abstraction used by Kafka to store events as a sequence of data items. Mostly these data items are referred as events and quiet rarely called as messages.<br>The data items are appended at the very end, and items which are already present are never changed! You never modify these logs.<br>In Kafka, logs are known as <strong><em>Topics. </em></strong>So topics are where our messages get accumulated. And ALWAYS remember, messages in Kafka are <strong><em>IMMUTABLE! </em></strong>Once you write them into a topic, you cannot modify them. Basically, its an event which has happened and you cannot rewrite history; as same as you cannot change your past, simple as that!</p><p>So, in a gist, a topic is analogous to a table in a database, and we have numerous tables in a database; similarly we can have numerous topics in a single Kafka cluster.<br><br><strong>NOTE: </strong>The messages in a topic can be of different format unlike a schema which needs to be followed in a database table.<br>The reason behind this is, that Kafka stores these messages as plain bytes. So format or schema doesn&apos;t matter.</p><p>How do you filter out messages out of a topic then?<br>Just like we deal with immutable data structures in various programming languages. Create a copy of that topic and filter out the unnecessary messages.<br><br><strong>MISCONCEPTION: </strong>Topics are <em>Logs. </em>Not <em>queues.</em><br>When a queue is read, the item is taken out of the queue and read. Now, nobody else can read that value. In a Kafka topic, when a message is read, it still stays intact. I can comeback after a few years and read that same message again!</p><p><strong>More on Kafka message:</strong><br>Its a <u>value</u>, associated with a <u>key</u>. <br>For example, consider this JSON:<br>{<br>    &quot;sensor_id&quot;: 42,<br>    &quot;location&quot;: &quot;Mumbai&quot;, <br>    &quot;temperature&quot;: 22, <br>    &quot;read_at&quot;: 1700<br>}<br>Here, the &apos;sensor_id&apos; becomes the key. This key is basically an identifier the payload relates to. Generally any unique id is taken as the key. Its not mandatory, but highly recommended to have a key! <br>We also got the <u>timestamp</u>, which tells us the moment the producer created that message.<br>The message also has some light-weight <u>headers</u>, and the most important parts of a Kafka message are <em>topic</em>, <em>partition</em> and <em>offset</em>!<br><br>Policies available in Kafka to prevent your disk from running out of space:<br>1. Log <strong>retention</strong>: delete old data or trim logs by size<br>2. Log <strong>compaction</strong>: keep only the latest value per key as sometimes context or history is irrelevant to maintain.<br><br><strong>How Kafka scales?</strong><br>If we limit our Kafka topic to a single node, then we will be restricting the topic&apos;s ability to scale to the node&apos;s disk space.<br>Since, Kafka is a distributed system, we partition the topic into several partitions.<br>Once partitions are created we need to decide to which partition the message goes to. <br>The message key helps us to make this decision. If the key is NULL, the messages are distributed in a round robin fashion. If not NULL and to ensure that a message with a certain key ALWAYS goes into a particular partition, hash the message key against a hash function mod the number of partitions and the output of this hash function is the partition number where the message goes to. This also helps us order our messages.<br><br><strong>A few terminologies:</strong></p><ol>
<li><strong>Cluster</strong>: A collection of one or more Kafka brokers working together to share the workload and provide redundancy (simply, a network of nodes).</li>
<li><strong>Node</strong>: The underlying physical or virtual infrastructure that hosts a broker (simply, machine with some disk space for storage purposes).</li>
<li><strong>Broker</strong>: A single Kafka server process that receives, stores, and fetches data (simply, a Kafka software process running on a node).</li>
</ol>
<p>Point to note here is that, a broker has access to disk space on that node on which it&apos;s running; this disk space is basically SSD which is tightly coupled next to the processor.</p>
<p>Brokers are responsible for handling incoming requests to write new messages to partitions as well as read messages out of them.<br><br><strong>Replication (for fault tolerance):</strong></p><figure class="kg-card kg-image-card"><img src="https://blog.gyri.tech/content/images/2026/06/Screenshot-from-2026-06-01-16-35-07.png" class="kg-image" alt="What is Kafka? (Part 1)" loading="lazy" width="1777" height="703" srcset="https://blog.gyri.tech/content/images/size/w600/2026/06/Screenshot-from-2026-06-01-16-35-07.png 600w, https://blog.gyri.tech/content/images/size/w1000/2026/06/Screenshot-from-2026-06-01-16-35-07.png 1000w, https://blog.gyri.tech/content/images/size/w1600/2026/06/Screenshot-from-2026-06-01-16-35-07.png 1600w, https://blog.gyri.tech/content/images/2026/06/Screenshot-from-2026-06-01-16-35-07.png 1777w" sizes="(min-width: 720px) 720px"></figure><p>What is Replication factor? <br>Number of copies created for each partition.<br><br>We set the replication factor (n), which in above case is 3. The darker one is termed as Leader (lead replica) whereas the remaining (n-1) copies are called as followers.<br>Messages are preferred to be written into and read from the leader but in rare scenarios we might write and read from a replica which is nearest to us in the network. <br>Meanwhile, as messages are written into the lead replica, the followers keep scrapping out the newly written messages from the leader to keep themselves updated and have everything replicated.<br><br>Now in case the Broker 1 fails, we still have copies of Partitions 0 (with brokers 2, 3) and 2 (with brokers 3, 4). <br>Here, Broker 1 had the leader of partition 0. So new leader will be elected for partition 0.<br><br><strong>PRODUCER:</strong><br>A producer is nothing but a Kafka client which writes data into the Kafka partition.<br><strong>CONSUMER:</strong><br>A consumer is a Kafka client that reads from the Kafka topic.<br><br>In short, anything which is not a broker, is, at the end of the day a producer or a consumer.<br><br>REFERENCE: <br>Apache Kafka 101 - <a href="https://developer.confluent.io/courses/apache-kafka/events/?ref=blog.gyri.tech">https://developer.confluent.io/courses/apache-kafka/events/</a></p>]]></content:encoded></item><item><title><![CDATA[The Complete Guide to Setting Up SSH Keys on Ubuntu, Windows, and macOS (With GitLab Integration)]]></title><description><![CDATA[<h1 id="step-1-understand-what-ssh-is">Step 1: Understand What SSH Is</h1><p>SSH stands for <strong>Secure Shell</strong>.<br>It is a secure protocol used to:</p><ul><li>Connect to remote servers</li><li>Clone Git repositories</li><li>Push and pull code securely</li><li>Deploy applications</li></ul><p>Instead of using passwords, SSH uses <strong>cryptographic key pairs</strong>.</p><hr><h1 id="step-2-understand-ssh-key-pair">Step 2: Understand SSH Key Pair</h1><p>When you create</p>]]></description><link>https://blog.gyri.tech/the-complete-guide-to-setting-up-ssh-keys-on-ubuntu-windows-and-macos-with-git-lab-integration/</link><guid isPermaLink="false">6996ab4de63ce6040c22526c</guid><dc:creator><![CDATA[AsifDesai]]></dc:creator><pubDate>Thu, 19 Feb 2026 06:51:15 GMT</pubDate><media:content url="https://blog.gyri.tech/content/images/2026/02/git-lab-ssh-key.webp" medium="image"/><content:encoded><![CDATA[<h1 id="step-1-understand-what-ssh-is">Step 1: Understand What SSH Is</h1><img src="https://blog.gyri.tech/content/images/2026/02/git-lab-ssh-key.webp" alt="The Complete Guide to Setting Up SSH Keys on Ubuntu, Windows, and macOS (With GitLab Integration)"><p>SSH stands for <strong>Secure Shell</strong>.<br>It is a secure protocol used to:</p><ul><li>Connect to remote servers</li><li>Clone Git repositories</li><li>Push and pull code securely</li><li>Deploy applications</li></ul><p>Instead of using passwords, SSH uses <strong>cryptographic key pairs</strong>.</p><hr><h1 id="step-2-understand-ssh-key-pair">Step 2: Understand SSH Key Pair</h1><p>When you create an SSH key, two files are generated:</p>
<!--kg-card-begin: html-->
<table data-start="876" data-end="1025" class="w-fit min-w-(--thread-content-width)"><thead data-start="876" data-end="894"><tr data-start="876" data-end="894"><th data-start="876" data-end="883" data-col-size="sm" class>File</th><th data-start="883" data-end="894" data-col-size="sm" class>Purpose</th></tr></thead><tbody data-start="915" data-end="1025"><tr data-start="915" data-end="973"><td data-start="915" data-end="930" data-col-size="sm"><code data-start="917" data-end="929">id_ed25519</code></td><td data-col-size="sm" data-start="930" data-end="973">Private key (Keep secret &#x274C; Never share)</td></tr><tr data-start="974" data-end="1025"><td data-start="974" data-end="993" data-col-size="sm"><code data-start="976" data-end="992">id_ed25519.pub</code></td><td data-col-size="sm" data-start="993" data-end="1025">Public key (Safe to share &#x2705;)</td></tr></tbody></table>
<!--kg-card-end: html-->
<p>How it works:</p><ol><li>You give your <strong>public key</strong> to GitLab.</li><li>GitLab stores it.</li><li>When you connect, GitLab verifies you using your <strong>private key</strong>.</li><li>If matched &#x2192; Access granted.<br><br>No password needed.</li></ol><hr><h1 id="step-3-why-we-use-ssh-in-gitlab">Step 3: Why We Use SSH in GitLab</h1><p>GitLab is used for:</p><ul><li>Hosting repositories</li><li>CI/CD</li><li>Version control</li><li>Collaboration</li></ul><p>Without SSH (using HTTPS):</p><p><code>git clone</code> https://gitlab.com/username/project.git<br>Every push asks for username &amp; password.</p><p>With SSH:<code>git clone</code> git@gitlab.com:username/project.git<br>Push &#x2192; Done instantly &#x1F680;<br>That&#x2019;s why developers prefer SSH.<br></p><hr><h1 id="step-4-generate-ssh-key-on-ubuntu">Step 4: Generate SSH Key on Ubuntu</h1><h3 id="1%EF%B8%8F%E2%83%A3-open-terminal">1&#xFE0F;&#x20E3; Open Terminal</h3><p>Press:<code>Ctrl + Alt + T</code><br><br>2&#xFE0F;&#x20E3; Check If SSH Already Exists<br><code>ls</code> -al ~/.ssh<br>If folder doesn&#x2019;t exist &#x2192; no problem.</p><h3 id="3%EF%B8%8F%E2%83%A3-generate-new-ssh-key">3&#xFE0F;&#x20E3; Generate New SSH Key</h3><p>Recommended modern algorithm:<br><code>ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;</code><br>If not supported:<br><code>ssh-keygen -t rsa -b 4096 -C &quot;your_email@example.com&quot;</code><br>Press Enter to accept default location:<code>/home/username/.ssh/</code>id_ed25519<br>Set passphrase (optional but recommended).<br><br>4&#xFE0F;&#x20E3; Start SSH Agent<br><code>eval &quot;$(ssh-agent -s)</code>&quot;<br><br>5&#xFE0F;&#x20E3; Add Key to SSH Agent<br>ssh-add ~/.ssh/id_ed25519</p><p>6&#xFE0F;&#x20E3; Copy Public Key<br><code>cat</code> ~/.ssh/id_ed25519.pub<br>Copy the entire output.</p><hr><h1 id="step-5-generate-ssh-key-on-windows">Step 5: Generate SSH Key on Windows</h1><h2 id="option-1-using-git-bash-recommended">Option 1: Using Git Bash (Recommended)</h2><ol><li>Install Git for Windows</li><li>Open Git Bash</li></ol><p>Run:<code>ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;</code><br>Keys are stored in:<code>C:\Users\YourUsername\.ssh\</code><br><br>View public key:<code>cat</code> ~/.ssh/id_ed25519.pub<br></p><h2 id="option-2-using-powershell">Option 2: Using PowerShell</h2><p>Open PowerShell and run:</p><p><code>ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;</code><br>Same process..</p><hr><h1 id="step-6-generate-ssh-key-on-macos">Step 6: Generate SSH Key on macOS</h1><h3 id="1%EF%B8%8F%E2%83%A3-open-terminal-1">1&#xFE0F;&#x20E3; Open Terminal</h3><p><code>Cmd + Space &#x2192; Terminal</code></p><h3 id="2%EF%B8%8F%E2%83%A3-generate-key">2&#xFE0F;&#x20E3; Generate Key</h3><p><code>ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;</code></p><h3 id="3%EF%B8%8F%E2%83%A3-start-ssh-agent">3&#xFE0F;&#x20E3; Start SSH Agent</h3><p><code>eval &quot;$(ssh-agent -s)</code>&quot;</p><h3 id="4%EF%B8%8F%E2%83%A3-add-key">4&#xFE0F;&#x20E3; Add Key</h3><p>ssh-add ~/.ssh/id_ed25519</p><h3 id="5%EF%B8%8F%E2%83%A3-copy-public-key">5&#xFE0F;&#x20E3; Copy Public Key</h3><p><code>pbcopy &lt; ~/.ssh/id_ed25519.pub</code></p><hr><h1 id="step-7-add-ssh-key-to-gitlab">Step 7: Add SSH Key to GitLab</h1><ol><li>Login to GitLab</li><li>Click Profile &#x2192; Preferences</li><li>Click <strong>SSH Keys</strong></li><li>Paste copied public key</li><li>Click <strong>Add Key</strong></li></ol><p>Now GitLab trusts your machine.</p><hr><h1 id="step-8-test-ssh-connection">Step 8: Test SSH Connection</h1><p>Run: ssh -T git@gitlab.com<br>If successful, you&#x2019;ll see:<code>Welcome to GitLab!</code></p><hr><h1 id="step-9-clone-using-ssh">Step 9: Clone Using SSH</h1><p>Instead of HTTPS:<br><code>git clone</code> git@gitlab.com:username/project.git<br>Now: git push<br>No password required.</p><hr><h1 id="step-10-security-best-practices"><strong>Ste</strong>p 10: Security Best Practices</h1><p>&#x2714; Use <code>ed25519</code><br>&#x2714; Protect private key<br>&#x2714; Add passphrase<br>&#x2714; Never share private key<br>&#x2714; Set correct permissions (Linux/macOS):</p><p><code>chmod</code> 700 ~/.ssh<br><code>chmod 600 ~/.ssh/id_ed25519</code></p><hr><h1 id="bonus-how-ssh-works-internally">Bonus: How SSH Works Internally</h1><ol><li>Client connects to GitLab</li><li>GitLab checks public key</li><li>GitLab sends encrypted challenge</li><li>Your private key decrypts it</li><li>Verified &#x2192; Access granted</li></ol><p>This uses asymmetric cryptography &#x2014; extremely secure.</p><hr><h1 id="final-conclusion">Final Conclusion</h1><p>Setting up SSH keys:</p><ul><li>Improves security</li><li>Removes password dependency</li><li>Enables automation</li><li>Essential for DevOps</li><li>Industry best practice</li></ul><p>If you are serious about professional development, SSH is not optional &#x2014; it&#x2019;s foundational</p>]]></content:encoded></item><item><title><![CDATA[A Complete Guide to Migrating to Spring Boot 4.0.1]]></title><description><![CDATA[<h2 id="technical-guide-upgrading-to-spring-boot-401">Technical Guide: Upgrading to Spring Boot 4.0.1</h2>
<p>This guide outlines the technical requirements, dependency changes, and configuration updates necessary to migrate a Spring Boot application to version 4.0.1.</p>
<p><strong>1. Build Environment Update</strong><br>
Before updating application dependencies, ensure your build tool is compatible with Spring Boot 4.</p>]]></description><link>https://blog.gyri.tech/spring-boot/</link><guid isPermaLink="false">697c526cec9cb1040bfad28c</guid><dc:creator><![CDATA[Aditya Methe]]></dc:creator><pubDate>Fri, 30 Jan 2026 07:27:01 GMT</pubDate><media:content url="https://blog.gyri.tech/content/images/2026/01/spring_boot_4.jpg" medium="image"/><content:encoded><![CDATA[<h2 id="technical-guide-upgrading-to-spring-boot-401">Technical Guide: Upgrading to Spring Boot 4.0.1</h2>
<img src="https://blog.gyri.tech/content/images/2026/01/spring_boot_4.jpg" alt="A Complete Guide to Migrating to Spring Boot 4.0.1"><p>This guide outlines the technical requirements, dependency changes, and configuration updates necessary to migrate a Spring Boot application to version 4.0.1.</p>
<p><strong>1. Build Environment Update</strong><br>
Before updating application dependencies, ensure your build tool is compatible with Spring Boot 4.0.1.<br>
<strong>Gradle Version</strong>: Update to Gradle 9.2.1. This version is required to support the updated Spring Boot plugins and dependencies.</p>
<p><strong>2. Dependency Management</strong><br>
Update your build.gradle.kts dependencies to their compatible versions.<br>
<strong>Spring Boot Starters</strong>: Ensure all standard starters are updated to the managed versions provided by the Spring Boot 4.0.1 BOM:</p>
<ul>
<li>spring-boot-starter-actuator</li>
<li>spring-boot-starter-data-mongodb-reactive</li>
<li>spring-boot-starter-security</li>
<li>spring-boot-starter-webflux</li>
<li>spring-boot-starter-oauth2-client</li>
</ul>
<p><strong>Third-Party Library Updates</strong>: You must manually update several key libraries to versions compatible with Spring Boot 4:</p>
<ul>
<li>Redisson: Update redisson-spring-boot-starter to 4.1.0.</li>
<li>SpringDoc OpenAPI (Swagger): Update to springdoc-openapi-starter-webflux-ui:3.0.1 to maintain API documentation support.</li>
</ul>
<p><strong>3. Breaking Change: Jackson 3 Migration</strong><br>
Spring Boot 4.0.1 moves from Jackson 2 to Jackson 3, which introduces significant namespace changes. This will cause compilation errors until references are updated.</p>
<p><strong>Dependency Changes</strong>: Replace the standard Kotlin module dependency:</p>
<p>// Remove or Update<br>
<code>implementation(&quot;com.fasterxml.jackson.module:jackson-module-kotlin&quot;)</code><br>
// Add New Tools Module<br>
<code>implementation(&quot;tools.jackson.module:jackson-module-kotlin&quot;)</code><br>
// Maintain backward compatibility if needed for specific libs<br>
<code>implementation(&quot;com.fasterxml.jackson.module:jackson-module-kotlin:2.20.1&quot;)</code></p>
<p><strong>Code Refactoring Required</strong>:</p>
<ul>
<li>
<p>Imports: Update your import statements for the object mapper.<br>
From: <code>com.fasterxml.jackson.module.kotlin.jacksonObjectMapper</code><br>
To: <code>tools.jackson.module.kotlin.jacksonObjectMapper</code></p>
</li>
<li>
<p>Codecs: If you are using Redisson or custom codecs, update <code>TypedJsonJacksonCodec</code> to <code>TypedJsonJackson3Codec</code>.</p>
</li>
</ul>
<p><strong>4. Configuration Properties Updates</strong><br>
Several properties in application.properties have been renamed or require explicit enablement.<br>
<strong>MongoDB Configuration:</strong> The <strong>spring.data.mongodb</strong> prefix has been simplified. You must remove the <strong>.data</strong> segment from your keys.</p>
<p>Full Mapping:</p>
<pre><code>spring.mongodb.host=192.168.1.122
spring.mongodb.port=27017
spring.mongodb.database=AppDBTest
spring.mongodb.username=admin
spring.mongodb.password=secret
spring.mongodb.authentication-database=admin
</code></pre>
<p><strong>Swagger UI</strong>: SpringDoc now requires explicit properties to enable the UI and API docs endpoints:</p>
<pre><code>springdoc.api-docs.enabled=true
springdoc.swagger-ui.enabled=true
</code></pre>
<p><strong>5. Tooling: Replacing Ktlint with Spotless</strong><br>
Older versions of ktlint may not be supported in this environment. It is recommended to switch to the Spotless Gradle plugin for code formatting.</p>
<p><strong>Implementation</strong>: Add the plugin to your build.gradle.kts:</p>
<pre><code>plugins {
    id(&quot;com.diffplug.spotless&quot;) version &quot;8.1.0&quot;
}
</code></pre>
<p><strong>Configuration</strong>: Configure Spotless to handle both Kotlin and Java formatting:</p>
<pre><code>spotless {
  kotlin {
    target(&quot;**/*.kt&quot;)
    ktfmt(&quot;0.53&quot;)
    trimTrailingWhitespace()
    endWithNewline()
  }

  java {
    target(&quot;**/*.java&quot;)
    googleJavaFormat(&quot;1.19.2&quot;)
    removeUnusedImports()
    trimTrailingWhitespace()
    endWithNewline()
  }

  // Bind to compilation tasks
  tasks.named(&quot;spotlessCheck&quot;) { dependsOn(&quot;compileKotlin&quot;, &quot;compileJava&quot;) }
  tasks.named(&quot;spotlessApply&quot;) { dependsOn(&quot;compileKotlin&quot;, &quot;compileJava&quot;) }
}
</code></pre>
<hr>]]></content:encoded></item><item><title><![CDATA[Nextcloud: The Master-Slave Guide]]></title><description><![CDATA[<h2 id="nextcloud-manual-setup-steps"><strong>Nextcloud manual setup steps</strong></h2><ul>
<li><strong>Update and Install Dependencies</strong></li>
</ul>
<p>We need the web server and all PHP modules required by Nextcloud.<br>
Run this on Server 1 AND Server 2:</p>
<pre><code>sudo apt update

sudo apt install apache2 php libapache2-mod-php php-mysql php-common php-gd php-curl php-imagick php-mbstring php-xml php-zip php-intl php-bcmath php-gmp unzip -y</code></pre>]]></description><link>https://blog.gyri.tech/nextcloud/</link><guid isPermaLink="false">69520346ec9cb1040bfad198</guid><dc:creator><![CDATA[Aditya Methe]]></dc:creator><pubDate>Mon, 29 Dec 2025 06:05:43 GMT</pubDate><media:content url="https://blog.gyri.tech/content/images/2025/12/nextcloud-scaled.jpg" medium="image"/><content:encoded><![CDATA[<h2 id="nextcloud-manual-setup-steps"><strong>Nextcloud manual setup steps</strong></h2><ul>
<li><strong>Update and Install Dependencies</strong></li>
</ul>
<img src="https://blog.gyri.tech/content/images/2025/12/nextcloud-scaled.jpg" alt="Nextcloud: The Master-Slave Guide"><p>We need the web server and all PHP modules required by Nextcloud.<br>
Run this on Server 1 AND Server 2:</p>
<pre><code>sudo apt update

sudo apt install apache2 php libapache2-mod-php php-mysql php-common php-gd php-curl php-imagick php-mbstring php-xml php-zip php-intl php-bcmath php-gmp unzip -y
</code></pre>
<ul>
<li><strong>Enable Required Apache Modules</strong></li>
</ul>
<p>Run this on Server 1 AND Server 2:</p>
<pre><code>sudo a2enmod rewrite headers env dir mime
sudo systemctl restart apache2
</code></pre>
<ul>
<li><strong>Create the Directory Structure</strong></li>
</ul>
<p>Run this on Server 1 AND Server 2:</p>
<pre><code>sudo mkdir -p /var/www/html/nextcloud
sudo mkdir -p /var/www/html/nextcloud/data
</code></pre>
<ul>
<li><strong>Set Permissions</strong></li>
</ul>
<p>Apache (www-data) must own these folders to write files.<br>
Run this on Server 1 AND Server 2:</p>
<pre><code>sudo chown -R www-data:www-data /var/www/html/nextcloud
sudo chmod -R 755 /var/www/html/nextcloud
</code></pre>
<ul>
<li><strong>Download and Unzip</strong></li>
</ul>
<p>Run this on Server 1 AND Server 2:</p>
<pre><code>cd /var/www/html
sudo wget https://download.nextcloud.com/server/releases/latest.zip
sudo unzip latest.zip
</code></pre>
<p>unzip will extract everything into the nextcloud folder we created</p>
<ul>
<li><strong>Final Permission Fix</strong></li>
</ul>
<p>Ensure the new files are owned by the web server.</p>
<pre><code>sudo chown -R www-data:www-data /var/www/html/nextcloud
</code></pre>
<ul>
<li><strong>Configure Apache</strong></li>
</ul>
<p>Run this on Server 1 AND Server 2 to create the web config:</p>
<pre><code>sudo nano /etc/apache2/sites-available/nextcloud.conf
</code></pre>
<p>Paste this content:</p>
<pre><code>&lt;VirtualHost *:80&gt;
    DocumentRoot /var/www/html/nextcloud
    
    &lt;Directory /var/www/html/nextcloud&gt;
        Require all granted
        AllowOverride All
        Options FollowSymLinks MultiViews
    &lt;/Directory&gt;
&lt;/VirtualHost&gt;
</code></pre>
<p>Enable the site:</p>
<pre><code>sudo a2ensite nextcloud.conf
sudo a2enmod rewrite
sudo systemctl restart apache2
</code></pre>
<hr><h2 id="database-setup-master-slave-replication"><strong>Database setup (Master slave replication)</strong></h2><ul>
<li><strong>Set up the Master (Server 1 Only)</strong></li>
</ul>
<ol>
<li>Secure the installation:</li>
</ol>
<pre><code>sudo mysql\_secure\_installation
</code></pre>
<ol start="2">
<li>Create the Database:</li>
</ol>
<pre><code>sudo mysql -u root -p
</code></pre>
<pre><code>CREATE DATABASE nextcloud;
CREATE USER &apos;nc\_admin&apos;@&apos;localhost&apos; IDENTIFIED BY &apos;12345&apos;;
GRANT ALL PRIVILEGES ON nextcloud.\* TO &apos;nc\_admin&apos;@&apos;localhost&apos;;

-- Create the User for Server 2 to copy data

CREATE USER &apos;replicator&apos;@&apos;%&apos; IDENTIFIED BY &apos;password&apos;;
GRANT REPLICATION SLAVE ON \*.\* TO &apos;replicator&apos;@&apos;%&apos;;
FLUSH PRIVILEGES;
</code></pre>
<ol start="3">
<li>Enable Binary Logging: Edit /etc/mysql/mariadb.conf.d/50-server.cnf:</li>
</ol>
<pre><code>bind-address = 0.0.0.0
server-id    = 1
log\_bin      = /var/log/mysql/mysql-bin.log
binlog\_do\_db = nextcloud
</code></pre>
<p>Restart &amp; Get Coordinates:</p>
<pre><code>sudo systemctl restart mysql
sudo mysql -u root -p -e &quot;SHOW MASTER STATUS;&quot;
</code></pre>
<ul>
<li><strong>Set up the Slave (Server 2 Only)</strong></li>
</ul>
<ol>
<li>
<p>Do NOT create a database. (It will copy Server 1&apos;s DB).</p>
</li>
<li>
<p>Configure Config File: Edit /etc/mysql/mariadb.conf.d/50-server.cnf:</p>
</li>
</ol>
<pre><code>bind-address = 0.0.0.0
server-id    = 2
log\_bin      = /var/log/mysql/mysql-bin.log
binlog\_do\_db = nextcloud
</code></pre>
<ol start="3">
<li>Restart:</li>
</ol>
<pre><code>sudo systemctl restart mysql
</code></pre>
<ul>
<li><strong>Connect Slave to Master</strong></li>
</ul>
<ol>
<li>Run on Server 2: Note: If servers are on different networks (192.168 vs 10.10), open the SSH tunnel first.</li>
</ol>
<pre><code>STOP SLAVE;
CHANGE MASTER TO
MASTER\_HOST=&apos;192.168.0.135&apos;, -- Or &apos;127.0.0.1&apos; if using Tunnel
MASTER\_USER=&apos;replicator&apos;,
MASTER\_PASSWORD=&apos;password&apos;,
MASTER\_LOG\_FILE=&apos;mysql-bin.000001&apos;, -- From Phase 2, Step 1
MASTER\_LOG\_POS=123;                 -- From Phase 2, Step 1
START SLAVE;
</code></pre>
<ol start="2">
<li>Copy Config to Server 2:</li>
</ol>
<p>On Server 1: Open /var/www/html/nextcloud/config/config.php and copy the text.</p>
<pre><code>sudo nano /var/www/html/nextcloud/config/config.php
</code></pre>
<p>On Server 2: Create the file:<br>
Paste the text.</p>
<pre><code>sudo nano /var/www/html/nextcloud/config/config.php
</code></pre>
<p>Important Change: Find the line &apos;dbhost&apos; =&gt; &apos;localhost&apos;, and ensure it stays localhost (so Server 2 reads its own replica DB).</p>
<hr><h2 id="file-synchronization-unison"><strong>File Synchronization (Unison)</strong></h2><ul>
<li><strong>Install Unison (Both Servers)</strong></li>
</ul>
<pre><code>sudo apt install unison -y
</code></pre>
<ul>
<li><strong>Configure SSH Key (Server 2 -&gt; Server 1)</strong></li>
</ul>
<pre><code>ssh-keygen -t ed25519
ssh-copy-id gyriexp@192.168.0.135
</code></pre>
<ul>
<li><strong>Create Sync Profile (Server 2)</strong><br>
Create ~/.unison/default.prf:</li>
</ul>
<pre><code>root = /var/www/html/nextcloud/data
root = ssh://gyriexp@192.168.0.135//var/www/html/nextcloud/data
auto = true
batch = true
prefer = newer
perms = 0     # Ignore permissions
</code></pre>
<ul>
<li><strong>Running the Sync</strong></li>
</ul>
<p>Run this sequence whenever you want to backup files:</p>
<ol>
<li>On Server 1 (Unlock):</li>
</ol>
<pre><code>sudo chown -R gyriexp:gyriexp /var/www/html/nextcloud/data
</code></pre>
<ol start="2">
<li>On Server 2 (Sync):</li>
</ol>
<pre><code>unison default
</code></pre>
<ol start="3">
<li>On Server 1 (Lock):</li>
</ol>
<pre><code>sudo chown -R www-data:www-data /var/www/html/nextcloud/data
</code></pre>
<h2 id="special-conditions"><strong>Special conditions</strong></h2><h3 id="server-1-fails-%E2%86%92-make-server-2-master-%E2%86%92-restore-server-1-as-slave">Server 1 Fails &#x2192; Make Server 2 Master &#x2192; Restore Server 1 as Slave</h3>
<ul>
<li><strong>Promote Server 2</strong></li>
</ul>
<p>Run these on Server 2 to make it the independent Master.</p>
<p>1.Stop it from looking for the dead master:</p>
<pre><code>sudo mysql -u root -p -e &quot;STOP SLAVE; RESET SLAVE ALL;&quot;
</code></pre>
<p>2.Enable Writes (if Read-Only was on)</p>
<pre><code>sudo mysql -u root -p -e &quot;SET GLOBAL read\_only = OFF;&quot;
</code></pre>
<ol start="3">
<li>Update Nextcloud Config: Ensure config.php points to localhost.</li>
</ol>
<pre><code>sudo nano /var/www/html/nextcloud/config/config.php
</code></pre>
<p>Ensure: &apos;dbhost&apos; =&gt; &apos;localhost&apos;,</p>
<ul>
<li><strong>Server 1 is Back (Make it the Slave)</strong></li>
</ul>
<ol>
<li>Create Backup on Server 2:</li>
</ol>
<p>Run on Server 2</p>
<pre><code>sudo mysqldump -u root -p --all-databases --master-data &gt; production\_backup.sql
</code></pre>
<ol start="2">
<li>Send Backup to Server 1:</li>
</ol>
<p>Run on Server 2</p>
<pre><code>scp production\_backup.sql user@server1\_ip:/home/user/
</code></pre>
<ol start="3">
<li>Import &amp; Configure Slave on Server 1:</li>
</ol>
<p>Run on Server 1</p>
<pre><code>sudo mysql -u root -p &lt; /home/user/production\_backup.sql
</code></pre>
<ol start="4">
<li>Start Replication on Server 1:</li>
</ol>
<p>Run on server 2</p>
<pre><code>sudo mysql -u root -p -e &quot;SHOW MASTER STATUS;&quot;
</code></pre>
<p>Run on server 1</p>
<pre><code>sudo mysql -u root -p
STOP SLAVE;
CHANGE MASTER TO
MASTER\_HOST=&apos;Server2\_IP&apos;,
MASTER\_USER=&apos;replicator&apos;,
MASTER\_PASSWORD=&apos;password&apos;,
MASTER\_LOG\_FILE=&apos;mysql-bin.00000X&apos;,  -- Value from Server 2
MASTER\_LOG\_POS=1234;                 -- Value from Server 2
START SLAVE;
</code></pre>
<ul>
<li><strong>Correct Unison File Sync</strong></li>
</ul>
<p>Since Unison is bidirectional, you usually don&apos;t need to change the command if the config is on Server 2. It will detect Server 2 has &quot;New files&quot; and Server 1 is empty/old, and it will push them automatically.</p>
<pre><code>\# Unlock Server 1
ssh user@server1\_ip &quot;sudo chown -R user:user /var/www/html/nextcloud/data&quot;

\# Run Sync (Pushes S2 data to S1)
unison default

\# Lock Server 1
ssh user@server1\_ip &quot;sudo chown -R www-data:www-data /var/www/html/nextcloud/data&quot;
</code></pre>
<p></p><h3 id="normal-chain-is-s1-%E2%86%92-s2-%E2%86%92-s3-server-2-dies-you-need-s1-to-sync-directly-to-s3-when-s2-returns-s3-updates-s2">Normal chain is S1 &#x2192; S2 &#x2192; S3. Server 2 dies. You need S1 to sync directly to S3. When S2 returns, S3 updates S2.</h3>
<ul>
<li><strong>Bypass Server 2 (S2 is Down)</strong></li>
</ul>
<ol>
<li>Create a &quot;Bypass Profile&quot; on Server 3:</li>
</ol>
<pre><code>nano ~/.unison/bypass.prf
</code></pre>
<p>Paste this:</p>
<pre><code>\# Sync Local (S3) with Server 1 (Skipping S2)
root = /var/www/html/nextcloud/data
root = ssh://user@server1\_ip//var/www/html/nextcloud/data
auto = true
batch = true
prefer = newer
perms = 0
</code></pre>
<ol start="2">
<li>Run the Bypass Sync: Run this on Server 3 whenever S2 is down.</li>
</ol>
<pre><code>\# 1. Unlock Server 1
ssh user@server1\_ip &quot;sudo chown -R user:user /var/www/html/nextcloud/data&quot;

\# 2. Sync S3 &lt;-&gt; S1
unison bypass

\# 3. Lock Server 1
ssh user@server1\_ip &quot;sudo chown -R www-data:www-data /var/www/html/nextcloud/data&quot;
</code></pre>
<ul>
<li><strong>Server 2 is Back (Restore &amp; Fill Gaps)</strong></li>
</ul>
<ol>
<li>Sync S3 &#x2192; S2 (Run on Server 3): We create a temporary profile to push data back to the recovered Server 2.</li>
</ol>
<pre><code>nano ~/.unison/restore\_s2.prf
</code></pre>
<pre><code>\# Sync Local (S3) to Server 2
root = /var/www/html/nextcloud/data
root = ssh://user@server2\_ip//var/www/html/nextcloud/data
auto = true
batch = true
prefer = newer
perms = 0
</code></pre>
<ol start="2">
<li>Run the Restore Sync:</li>
</ol>
<pre><code>\# 1. Unlock Server 2 (The recovered server)
ssh user@server2\_ip &quot;sudo chown -R user:user /var/www/html/nextcloud/data&quot;

\# 2. Sync S3 -&gt; S2
unison restore\_s2

\# 3. Lock Server 2
ssh user@server2\_ip &quot;sudo chown -R www-data:www-data /var/www/html/nextcloud/data&quot;
</code></pre>
<ol start="3">
<li>Resume Normal Operations: Now that S2 is up-to-date, you can go back to your original setup (S1 syncing to S2, and S2 syncing to S3).</li>
</ol>
<ul>
<li><strong>step 1: Verify Server 2 is &quot;Clean&quot;</strong></li>
</ul>
<p>Run on Server 2:</p>
<pre><code>ls -l /var/www/html/nextcloud/data/
</code></pre>
<ul>
<li><strong>Step 2: Resume Server 1 &#x2192; Server 2 Sync</strong></li>
</ul>
<ol>
<li>Unlock Server 2<br>
Run on Server 1</li>
</ol>
<pre><code>ssh user@server2_ip &quot;sudo chown -R user:user /var/www/html/nextcloud/data&quot;
</code></pre>
<ol start="2">
<li>ssh user@server2_ip &quot;sudo chown -R user:user /var/www/html/nextcloud/data&quot;</li>
</ol>
<pre><code>unison default
</code></pre>
<ol start="3">
<li>Lock Server 2:</li>
</ol>
<pre><code>ssh user@server2_ip &quot;sudo chown -R www-data:www-data /var/www/html/nextcloud/data&quot;
</code></pre>
<ul>
<li><strong>Step 3: Resume Server 2 &#x2192; Server 3 Sync</strong></li>
</ul>
<p>Now that S2 is updated by S1, it passes those updates down to S3.<br>
Run on Server 2: (Assuming you have a profile named sync_to_s3 that points to S3).</p>
<p>Unlock Server 3:</p>
<pre><code>ssh user@server3_ip &quot;sudo chown -R user:user /var/www/html/nextcloud/data&quot;
</code></pre>
<p>Run the Standard Sync:</p>
<pre><code>unison sync_to_s3
</code></pre>
<p>Lock Server 3:</p>
<pre><code>ssh user@server3_ip &quot;sudo chown -R www-data:www-data /var/www/html/nextcloud/data&quot;
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Designing a Reactive, Event-Driven Email OTP Login System with CQRS and Event Sourcing]]></title><description><![CDATA[<p>Modern authentication systems are moving beyond passwords. With the rise of <strong>passwordless authentication</strong>, <strong>One-Time Passwords (OTPs)</strong> delivered over email or SMS are becoming a trusted standard for secure and seamless user logins.</p><p>However, implementing an OTP-based login mechanism that is <strong>scalable, reactive, and audit-compliant</strong> requires more than a simple verification</p>]]></description><link>https://blog.gyri.tech/building-a-reactive-event-driven-otp-login-system-with-spring-boot-cqrs-event-sourcing/</link><guid isPermaLink="false">691303ee7c25ab040d0c2db6</guid><dc:creator><![CDATA[Sangram Ekshinge]]></dc:creator><pubDate>Tue, 11 Nov 2025 11:06:10 GMT</pubDate><media:content url="https://blog.gyri.tech/content/images/2025/11/Screenshot-2025-11-11-163110-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://blog.gyri.tech/content/images/2025/11/Screenshot-2025-11-11-163110-1.png" alt="Designing a Reactive, Event-Driven Email OTP Login System with CQRS and Event Sourcing"><p>Modern authentication systems are moving beyond passwords. With the rise of <strong>passwordless authentication</strong>, <strong>One-Time Passwords (OTPs)</strong> delivered over email or SMS are becoming a trusted standard for secure and seamless user logins.</p><p>However, implementing an OTP-based login mechanism that is <strong>scalable, reactive, and audit-compliant</strong> requires more than a simple verification service. It demands an architecture that supports <strong>asynchronous workflows</strong>, <strong>event traceability</strong>, and <strong>reactive scalability</strong> &#x2014; all while keeping the system maintainable and extendable.</p><p>In this post, we&#x2019;ll walk through the <strong>conceptual design</strong> of a <strong>Reactive OTP Login System</strong> built using <strong>Spring Boot</strong>, <strong>CQRS (Command Query Responsibility Segregation)</strong>, and <strong>Event Sourcing</strong>.<br>We&#x2019;ll focus purely on the architecture and system flow &#x2014; without diving into code &#x2014; to illustrate how such a system achieves <strong>high throughput</strong>, <strong>clean separation of concerns</strong>, and <strong>full auditability</strong>.</p><h2 id="architectural-overview">Architectural Overview</h2><p>At its core, this system adopts a <strong>Reactive CQRS + Event Sourcing</strong> design:</p><ul><li><strong>Commands</strong> modify the system state (e.g., logging in a user).</li><li><strong>Queries</strong> fetch or compute data without altering state (e.g., generating OTP for a user).</li><li><strong>Events</strong> record every state transition as an immutable fact.</li><li><strong>Reactive Streams (Mono/Flux)</strong> ensure non-blocking, backpressure-aware execution.</li></ul><h2 id="step-1-requesting-an-email-otp">Step 1: Requesting an Email OTP</h2><p>The OTP generation process begins when a user initiates a login using their email.</p><h3 id="conceptual-flow">Conceptual Flow</h3><ol><li>The <strong>client</strong> sends a request to <code>/api/v1/user/request/emailloginotp/{email}</code>.</li><li>The <strong>Query Controller</strong> constructs a <code>RequestEmailLoginOTPQuery</code>, encapsulating the user&#x2019;s email and tenant metadata.</li><li>The <strong>Query Dispatcher</strong> routes the query to the appropriate <strong>Query Handler</strong>.</li><li>The <strong>Query Handler</strong> verifies whether the user exists and is active using a <strong>reactive repository</strong>.</li><li>If valid, it invokes <strong>EmailLoginOTPService</strong> to generate a unique OTP.</li><li>It then emits an <strong>EmailOTPSentEvent</strong>, which is published to <strong>Kafka</strong> for asynchronous notification delivery and audit tracking.</li><li>The API responds with a confirmation that the OTP request was processed successfully.</li></ol><h2 id="step-2-validating-the-email-otp-login-process">Step 2: Validating the Email OTP (Login Process)</h2><p>Once the user receives the OTP, they can authenticate using it.</p><h3 id="conceptual-flow-1">Conceptual Flow</h3><ol><li>The <strong>client</strong> submits a POST request to <code>/api/v1/user/validate/emailloginotp</code> with <code>email</code> and <code>otp</code>.</li><li>The <strong>Command Controller</strong> validates the OTP through the <strong>EmailLoginOTPService</strong>.</li><li>On success, it dispatches a <strong>LogInWithEmailOTPCommand</strong> to the <strong>Command Handler</strong>.</li><li>The <strong>Command Handler</strong> performs several key actions:<ul><li>Rehydrates the user&#x2019;s aggregate state from the <strong>Event Store</strong>.</li><li>Raises a <strong>LoggedInWithEmailOTPEvent</strong> to represent successful login.</li><li>Persists this event back to the <strong>Event Store</strong>.</li></ul></li><li>The <strong>Event Handler</strong> listens for the event, updating the <strong>read projection</strong> (user table) to reflect login status.</li><li>The <strong>JWT Token Provider</strong> issues a new <strong>access token</strong> for the authenticated session.</li><li>The client receives a success response along with the token.</li></ol><h2 id="event-sourcing-lifecycle">Event Sourcing Lifecycle</h2><p>Event Sourcing lies at the heart of this system.<br>Every change in user state &#x2014; from OTP sent to login success &#x2014; is captured as a <strong>domain event</strong> and stored immutably in the <strong>Event Store</strong>.</p><h3 id="conceptual-event-chain">Conceptual Event Chain</h3>
<!--kg-card-begin: html-->
<table data-start="6159" data-end="6635" class="w-fit min-w-(--thread-content-width)"><thead data-start="6159" data-end="6253"><tr data-start="6159" data-end="6253"><th data-start="6159" data-end="6170" data-col-size="sm">Sequence</th><th data-start="6170" data-end="6198" data-col-size="sm">Event Name</th><th data-start="6198" data-end="6253" data-col-size="md">Description</th></tr></thead><tbody data-start="6349" data-end="6635"><tr data-start="6349" data-end="6443"><td data-start="6349" data-end="6360" data-col-size="sm">1</td><td data-start="6360" data-end="6388" data-col-size="sm"><code data-start="6362" data-end="6381">EmailOTPSentEvent</code></td><td data-start="6388" data-end="6443" data-col-size="md">OTP generated and dispatched via Kafka.</td></tr><tr data-start="6444" data-end="6540"><td data-start="6444" data-end="6455" data-col-size="sm">2</td><td data-start="6455" data-end="6485" data-col-size="sm"><code data-start="6457" data-end="6484">LoggedInWithEmailOTPEvent</code></td><td data-start="6485" data-end="6540" data-col-size="md">User successfully logged in with the OTP.</td></tr><tr data-start="6541" data-end="6635"><td data-start="6541" data-end="6552" data-col-size="sm">3</td><td data-start="6552" data-end="6580" data-col-size="sm"><code data-start="6554" data-end="6574">AuditRecordedEvent</code></td><td data-start="6580" data-end="6635" data-col-size="md">Login and OTP actions recorded for compliance.</td></tr></tbody></table>
<!--kg-card-end: html-->
<p>Replaying these events allows the system to <strong>rebuild state from history</strong>, ensuring <strong>full traceability</strong> and <strong>fault recovery</strong> without traditional database state dependencies.</p><h2 id="security-and-compliance-considerations">Security and Compliance Considerations</h2><p>Security is baked into every stage of the pipeline:</p><ul><li><strong>OTP expiration, uniqueness, and rate limiting</strong> are handled by the <code>EmailLoginOTPService</code>.</li><li><strong>JWT tokens</strong> provide stateless, secure session management.</li><li><strong>Audit events</strong> ensure all critical actions are logged for compliance.</li><li><strong>Tenant-aware scoping</strong> enforces strict data isolation in multi-tenant setups.</li></ul><p>By combining event logs with JWT-based access, the system achieves both <strong>security</strong> and <strong>observability</strong>.</p><h2 id="conclusion">Conclusion</h2><p>Building an OTP-based authentication system using <strong>Reactive</strong>, <strong>CQRS</strong>, and <strong>Event Sourcing</strong> principles enables:</p><p><strong>Scalable, non-blocking performance</strong> under heavy traffic<br><strong>Strict auditability</strong> with event logs<br><strong>Fault tolerance</strong> through event replay<br><strong>Extensibility</strong> for future login channels or audit pipelines<br><strong>Seamless user experience</strong> with real-time responsiveness</p><p>In essence, this architecture transforms a simple OTP mechanism into a <strong>production-grade authentication framework</strong> &#x2014; one that is reactive by nature, event-driven by design, and compliant by default.</p>]]></content:encoded></item><item><title><![CDATA[Securely Embedding Documents in Excel: The Short URL Pattern]]></title><description><![CDATA[<h2 id="the-problem">The Problem</h2>
<p>Enterprise applications that export Excel reports with embedded documents face a critical security challenge. The straightforward implementation exposes internal system identifiers:</p>
<pre><code>https://app.example.com/api/document/12345/view?tenantId=67890
</code></pre>
<p>This reveals:</p>
<ul>
<li>Internal document IDs (12345)</li>
<li>Tenant identifiers (67890)</li>
<li>API structure and endpoints</li>
</ul>
<p>Once Excel files are</p>]]></description><link>https://blog.gyri.tech/the-secure-redirect-pattern-the-real-way-to-link-to-secure-documents-in-excel/</link><guid isPermaLink="false">69034b7a35fde4040e7a7ae4</guid><dc:creator><![CDATA[Aditya Methe]]></dc:creator><pubDate>Thu, 30 Oct 2025 13:46:27 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1590859808308-3d2d9c515b1a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHdlYiUyMGxpbmt8ZW58MHx8fHwxNzYxODMxOTMwfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<h2 id="the-problem">The Problem</h2>
<img src="https://images.unsplash.com/photo-1590859808308-3d2d9c515b1a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHdlYiUyMGxpbmt8ZW58MHx8fHwxNzYxODMxOTMwfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="Securely Embedding Documents in Excel: The Short URL Pattern"><p>Enterprise applications that export Excel reports with embedded documents face a critical security challenge. The straightforward implementation exposes internal system identifiers:</p>
<pre><code>https://app.example.com/api/document/12345/view?tenantId=67890
</code></pre>
<p>This reveals:</p>
<ul>
<li>Internal document IDs (12345)</li>
<li>Tenant identifiers (67890)</li>
<li>API structure and endpoints</li>
</ul>
<p>Once Excel files are distributed, these identifiers persist indefinitely, creating ongoing security exposure even if the URLs require authentication.</p>
<p>The challenge intensifies when requirements include:</p>
<ul>
<li>Embedding images directly in Excel cells</li>
<li>Providing download links for full-resolution access</li>
<li>Supporting non-image documents (PDFs, DOCX) as clickable links</li>
</ul>
<h2 id="solution-short-url-redirection">Solution: Short URL Redirection</h2>
<p>Replace direct URLs with temporary, opaque redirect tokens:</p>
<p><strong>Before:</strong></p>
<pre><code>https://app.example.com/api/document/12345/view?tenantId=67890
</code></pre>
<p><strong>After:</strong></p>
<pre><code>https://app.example.com/api/shorturl/7k9x-m2p4
</code></pre>
<p>The short URL provides:</p>
<ul>
<li>Complete opacity (no exposed identifiers)</li>
<li>Automatic expiration (configurable timeframe)</li>
<li>Full auditability (access tracking)</li>
<li>Universal compatibility (images and documents)</li>
</ul>
<hr>
<h2 id="architecture">Architecture</h2>
<h3 id="phase-1-report-generation">Phase 1: Report Generation</h3>
<p><strong>Token Generation:</strong></p>
<p>For each document in the export:</p>
<ol>
<li>Generate cryptographically secure random token (e.g., <code>7k9x-m2p4</code>)</li>
<li>Store mapping in database:</li>
</ol>
<pre><code>short_code:    7k9x-m2p4
original_url:  /api/document/12345/view?tenantId=67890
expires_at:    2025-11-06 14:30:00 UTC
created_by:    user@example.com
</code></pre>
<p><strong>Content Handling:</strong></p>
<p><strong>Images:</strong></p>
<ul>
<li>Fetch image via secure internal URL (server-side)</li>
<li>Embed binary data directly into Excel cell</li>
<li>Attach short URL as clickable hyperlink</li>
<li>Result: Thumbnail preview with download link</li>
</ul>
<p><strong>Non-Images:</strong></p>
<ul>
<li>Insert short URL as clickable hyperlink only</li>
<li>Result: Text link for download/viewing</li>
</ul>
<p><strong>Excel Generation:</strong></p>
<p>Use server-side libraries (Apache POI, OpenPyXL, EPPlus) to:</p>
<ul>
<li>Embed image binary data in cells</li>
<li>Configure hyperlinks to short URLs</li>
<li>Ensure zero internal identifiers in output</li>
</ul>
<h3 id="phase-2-document-access">Phase 2: Document Access</h3>
<p><strong>User clicks link in Excel:</strong></p>
<ol>
<li>
<p><strong>Request</strong>: Browser opens <code>GET /api/shorturl/7k9x-m2p4</code></p>
</li>
<li>
<p><strong>Validation</strong>: Public endpoint checks:</p>
<ul>
<li>Token exists in database</li>
<li>Token not expired</li>
<li>Log access attempt</li>
</ul>
</li>
<li>
<p><strong>Redirect</strong>: Return HTTP 302:</p>
<pre><code>Location: https://app.example.com/api/document/12345/view?tenantId=67890
</code></pre>
</li>
<li>
<p><strong>Authentication</strong>: Browser follows redirect with existing session, normal authentication applies</p>
</li>
</ol>
<p><strong>User experience:</strong> Click &#x2192; Document opens</p>
<p><strong>System process:</strong> Validate &#x2192; Redirect &#x2192; Authenticate &#x2192; Deliver</p>
<hr>
<h2 id="technical-implementation">Technical Implementation</h2>
<h3 id="database-schema">Database Schema</h3>
<table>
<thead>
<tr>
<th>Column</th>
<th>Type</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>short_code</code></td>
<td>VARCHAR(32), PRIMARY KEY</td>
<td>Unique token identifier</td>
</tr>
<tr>
<td><code>original_url</code></td>
<td>TEXT</td>
<td>Target document URL</td>
</tr>
<tr>
<td><code>expires_at</code></td>
<td>TIMESTAMP</td>
<td>Expiration timestamp</td>
</tr>
<tr>
<td><code>created_by</code></td>
<td>VARCHAR(255)</td>
<td>Originating user</td>
</tr>
<tr>
<td><code>click_count</code></td>
<td>INTEGER</td>
<td>Access counter</td>
</tr>
</tbody>
</table>
<h3 id="security-configuration">Security Configuration</h3>
<p><strong>Redirect Endpoint:</strong></p>
<ul>
<li>Path: <code>/api/shorturl/**</code></li>
<li>Authentication: None (public access)</li>
<li>Rate limiting: Required (prevents enumeration)</li>
<li>Validation: Token existence and expiration only</li>
</ul>
<p><strong>Token Specification:</strong></p>
<ul>
<li>Length: Minimum 12 alphanumeric characters</li>
<li>Entropy: Cryptographically secure random generation</li>
<li>Character set: URL-safe (a-z, A-Z, 0-9)</li>
<li>Collision handling: Database unique constraint with retry</li>
</ul>
<h3 id="expiration-policy">Expiration Policy</h3>
<table>
<thead>
<tr>
<th>Use Case</th>
<th>Recommended Period</th>
</tr>
</thead>
<tbody>
<tr>
<td>Internal reports</td>
<td>7 days</td>
</tr>
<tr>
<td>External sharing</td>
<td>24-48 hours</td>
</tr>
<tr>
<td>Archive/compliance</td>
<td>30 days</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="security-benefits">Security Benefits</h2>
<h3 id="1-information-protection">1. Information Protection</h3>
<ul>
<li>Internal IDs completely hidden</li>
<li>API structure remains confidential</li>
<li>Zero architectural details exposed</li>
</ul>
<h3 id="2-temporal-controls">2. Temporal Controls</h3>
<ul>
<li>Automatic expiration reduces long-term risk</li>
<li>Distributed files lose access over time</li>
<li>No indefinite credential exposure</li>
</ul>
<h3 id="3-audit-capability">3. Audit Capability</h3>
<ul>
<li>Track every access attempt</li>
<li>Record timestamp, IP, user identity</li>
<li>Enable anomaly detection and compliance reporting</li>
</ul>
<h3 id="4-access-management">4. Access Management</h3>
<ul>
<li>Revoke tokens immediately if needed</li>
<li>Extend expiration programmatically</li>
<li>Tenant-level and user-level controls</li>
</ul>
<hr>
<h2 id="performance-considerations">Performance Considerations</h2>
<p><strong>Token Generation:</strong></p>
<ul>
<li>Batch create during report generation</li>
<li>~1ms per token generation</li>
<li>Minimal database overhead</li>
</ul>
<p><strong>Lookup Performance:</strong></p>
<ul>
<li>O(1) with proper indexing</li>
<li>Cache active tokens (optional)</li>
<li>Typical response time: &lt;10ms</li>
</ul>
<p><strong>Cleanup:</strong></p>
<ul>
<li>Schedule periodic deletion of expired tokens</li>
<li>Run during off-peak hours</li>
<li>Archive audit data before deletion</li>
</ul>
<p><strong>Image Processing:</strong></p>
<ul>
<li>Server-side fetch and embed: ~50-100ms per image</li>
<li>Memory efficient with streaming</li>
<li>No browser limitations</li>
</ul>
<hr>
<h2 id="example-workflow">Example Workflow</h2>
<p><strong>Scenario:</strong> Report with 30 images, 20 PDFs</p>
<p><strong>Generation:</strong></p>
<ol>
<li>Generate 50 short URLs: ~50ms</li>
<li>Fetch and embed 30 images: ~2-3 seconds</li>
<li>Build Excel file: ~500ms</li>
<li>Total: ~3-4 seconds</li>
</ol>
<p><strong>Usage:</strong></p>
<ul>
<li>5 users, 7-day period</li>
<li>150 total accesses across 50 documents</li>
<li>Complete audit trail maintained</li>
<li>Zero ID exposure</li>
<li>Automatic expiration after policy period</li>
</ul>
<hr>
<h2 id="implementation-checklist">Implementation Checklist</h2>
<p><strong>Required Components:</strong></p>
<ul>
<li>Short URL mapping database table with proper indexes</li>
<li>Token generation service (cryptographically secure)</li>
<li>Public redirect endpoint (unauthenticated)</li>
<li>Token validation logic (existence + expiration)</li>
<li>Audit logging mechanism</li>
<li>Scheduled cleanup job for expired tokens</li>
</ul>
<p><strong>Security Requirements:</strong></p>
<ul>
<li>Rate limiting on redirect endpoint</li>
<li>Cryptographic random token generation</li>
<li>Proper expiration policy configuration</li>
<li>HTTP 302 redirect (not 301 permanent)</li>
<li>Error handling (404 for invalid/expired tokens)</li>
</ul>
<p><strong>Server-Side Excel Library:</strong></p>
<ul>
<li>Java: Apache POI</li>
<li>Python: OpenPyXL</li>
<li>.NET: EPPlus</li>
</ul>
<p><strong>Why Server-Side:</strong><br>
Browser-based libraries (xlsx-js-style, etc.) cannot embed images in Excel cells. Server-side libraries provide full binary embedding capability.</p>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>The short URL redirection pattern solves document embedding security through architectural indirection. By replacing direct URLs with temporary tokens, systems achieve:</p>
<ul>
<li><strong>Complete identifier masking</strong> (no IDs exposed)</li>
<li><strong>Automatic expiration</strong> (temporal security)</li>
<li><strong>Full auditability</strong> (compliance ready)</li>
<li><strong>Seamless UX</strong> (transparent to users)</li>
</ul>
<p>This approach is applicable beyond Excel exports&#x2014;anywhere temporary, auditable access to secure resources is required without exposing internal architecture.</p>
<p><strong>Key Takeaway:</strong> Never expose internal identifiers in exported files. Use opaque, expiring tokens as an indirection layer between distributed content and secure resources.</p>
<hr>
<p><strong>Technical Stack Considerations:</strong></p>
<p>This pattern integrates with existing authentication infrastructure. The redirect endpoint bypasses authentication (public), while the target URL enforces normal security. Token validation provides the security boundary, relying on cryptographic randomness and expiration rather than session state.</p>
<p><strong>Broader Applications:</strong> PDF reports, email notifications, third-party integrations, mobile apps, automated distribution systems&#x2014;any context requiring temporary resource access without persistent authentication.</p>
]]></content:encoded></item><item><title><![CDATA[MongoDB Commands]]></title><description><![CDATA[<h2 id="commands">Commands</h2><ul><li><code>mongod</code> - start mongo db server instance</li><li><code>mongo</code> - start mongo db command prompt to execute commands</li><li><code>show dbs</code> - show databases</li><li><code>mongoimport -d [DATABASE_NAME] -c [COLLECTIONS] --file [FILE_NAME]</code> - import into database and collection from file</li><li><code>mongoexport -d [DATABASE_NEM] -c [COLLECTIONS] --out [FILE_NAME]</code> -</li></ul>]]></description><link>https://blog.gyri.tech/mongodb-commands/</link><guid isPermaLink="false">68ec6c3bd1e62b04108b4ceb</guid><dc:creator><![CDATA[Kaustubh Kesarkar]]></dc:creator><pubDate>Mon, 13 Oct 2025 03:06:19 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1658204238967-3a81a063d162?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fG1vbmdvZGJ8ZW58MHx8fHwxNzYwMzI0NzE2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<h2 id="commands">Commands</h2><ul><li><code>mongod</code> - start mongo db server instance</li><li><code>mongo</code> - start mongo db command prompt to execute commands</li><li><code>show dbs</code> - show databases</li><li><code>mongoimport -d [DATABASE_NAME] -c [COLLECTIONS] --file [FILE_NAME]</code> - import into database and collection from file</li><li><code>mongoexport -d [DATABASE_NEM] -c [COLLECTIONS] --out [FILE_NAME]</code> - export db in json</li><li><code>mongodump</code> - binary export</li><li><code>mongorestore</code> - restore binary export</li><li><code>bsondump</code></li><li><code>mongostat</code></li><li><code>use [DB_NAME]</code>- change/switch DB. There is NO command to create DB</li><li><code>db</code> - set variable as DB (after <em>user [DB_NAME]</em>)</li><li><code>db.[COLLECTIONS]</code> - e.g. db.links. Don&apos;t have to create collections (similar to DB)</li><li><code>db.[COLLECTIONS].count()</code> - to check number of documents. e.g. db.links.count()</li><li><code>db.[COLLECTIONS].insert({[JSON_DATA]})</code> - e.g. db.links.insert({title: &quot;google search&quot;, url:&quot;www.google.com&quot;, comment:&quot;top search engine&quot;, tags:[&quot;default&quot;,&quot;main page&quot;], saved_on: new Date()})</li><li><code>db.[COLLECTIONS].save({[JSON_DATA]})</code> - e.g. db.links.save({title: &quot;yahoo search&quot;, url: &quot;www.yahoo.com&quot;, saved_on: new Date()})</li><li><code>db.[COLLECTIONS].find()</code> - e.g. db.links.find(). Query the database to find the documents</li></ul><h3 id="examples">Examples</h3><ul><li><code>show collections</code> - show collections for db</li><li><code>db.users.insert({&quot;name&quot;:&quot;username&quot;})</code> - insert collection user with data</li><li><code>db.users.count()</code> - show count of collections</li><li><code>db.users.find()</code> - find all data from collection</li><li><code>db.users.find().explain()</code> - explain execution planner for query to find data</li><li><code>db.users.find().explain(&quot;executionStats&quot;)</code> - more information on explain</li><li><code>db.users.createIndex({number:1})</code> - to create an index on collection</li><li><code>db.users.update({&quot;name&quot;:&quot;username&quot;}, $set: {&quot;first_name&quot;:&quot;Name&quot;})&quot;</code> - <em>$set</em> update record</li><li><code>db.users.update({&quot;name&quot;:&quot;username&quot;}, {$addToSet: {&quot;hobbies&quot;:&quot;running&quot;}})&quot;</code> - <em>$addToSet</em> update array with new value</li><li><code>db.users.remove({&quot;name&quot;:&quot;username&quot;})</code> - remove record</li><li><code>db.users.drop()</code> - drop entire <em>users</em> collection</li></ul><h3 id="notes">Notes</h3><ul><li><strong>bson</strong>: binary JSON</li><li><strong>Collection</strong>: Like an RDBMS table; but collection has no schemas.</li><li><strong>Document</strong>: Like an RDBMS record or row. There are bson documents.</li><li><strong>Field</strong>: Like an RDBMS column; {key: value}</li></ul><hr>]]></content:encoded></item><item><title><![CDATA[react-native-document-picker]]></title><description><![CDATA[<p>The <code>react-native-document-picker</code> library is a React Native module that allows users to select documents (or files) from their device storage. This can include various types of files, such as images, videos, PDFs, audio files, and other types of documents, depending on the platform (iOS or Android).</p><p>The primary use case</p>]]></description><link>https://blog.gyri.tech/react-native-document-picker/</link><guid isPermaLink="false">67441a29c1ace803d59102c3</guid><dc:creator><![CDATA[Ankita Shelake]]></dc:creator><pubDate>Mon, 13 Oct 2025 02:56:12 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1544396821-4dd40b938ad3?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDd8fGZpbGVzfGVufDB8fHx8MTc2MDMyNDE0Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1544396821-4dd40b938ad3?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDd8fGZpbGVzfGVufDB8fHx8MTc2MDMyNDE0Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="react-native-document-picker"><p>The <code>react-native-document-picker</code> library is a React Native module that allows users to select documents (or files) from their device storage. This can include various types of files, such as images, videos, PDFs, audio files, and other types of documents, depending on the platform (iOS or Android).</p><p>The primary use case for this library is to give users the ability to pick files from their local storage or cloud services (like iCloud, Google Drive, etc.) within a React Native app.</p><h3 id="key-features-of-react-native-document-picker">Key Features of <code>react-native-document-picker</code>:</h3><ol><li><strong>File Selection</strong>:<ul><li>Allows users to pick various types of files (documents, images, audio, etc.) from their device storage.</li><li>Works across iOS and Android, providing a consistent API to access documents.</li></ul></li><li><strong>File Type Filtering</strong>:<ul><li>You can specify the types of files you want users to select (e.g., only PDFs, only images, only audio files, etc.).</li><li>The library provides predefined constants for file types (e.g., <code>DocumentPicker.types.images</code>, <code>DocumentPicker.types.audio</code>, <code>DocumentPicker.types.pdf</code>), but you can also specify custom types.</li></ul></li><li><strong>Multiple File Selection</strong>:<ul><li>You can allow the user to select multiple files at once (in supported platforms).</li></ul></li><li><strong>File Metadata</strong>:<ul><li>After selecting a file, the library provides metadata about the file, such as its URI, name, size, type, and more, which you can use in your app.</li></ul></li><li><strong>Cross-platform</strong>:<ul><li>Works on both <strong>iOS</strong> and <strong>Android</strong> devices, though there are some differences in behavior between the platforms, especially regarding the supported file types and the appearance of the file picker.</li></ul></li></ol><h3 id="basic-usage-example">Basic Usage Example:</h3><p>Here&#x2019;s a simple example of how to use the <code>react-native-document-picker</code> library to allow a user to pick an audio file:</p><p><strong>Import and Use the Library</strong>:Below is an example of how to use <code>react-native-document-picker</code> to allow the user to pick an audio file:</p><pre><code class="language-javascript">import React, { useState } from &apos;react&apos;;
import { View, Button, Text, Alert } from &apos;react-native&apos;;
import DocumentPicker from &apos;react-native-document-picker&apos;;

const FilePickerExample = () =&gt; {
  const [fileInfo, setFileInfo] = useState(null);

  // Function to pick an audio file
  const handlePickFile = async () =&gt; {
    try {
      // Pick an audio file
      const res = await DocumentPicker.pick({
        type: [DocumentPicker.types.audio],  // Filter for audio files
      });

      setFileInfo(res);  // Set the file metadata

      // Alert with file information
      Alert.alert(&apos;File Selected&apos;, `File: ${res.name}, Size: ${res.size} bytes`);

      console.log(res);  // Log the file metadata (URI, type, name, etc.)
    } catch (err) {
      if (DocumentPicker.isCancel(err)) {
        console.log(&apos;User canceled the picker&apos;);
      } else {
        console.error(&apos;Error picking file:&apos;, err);
      }
    }
  };

  return (
    &lt;View style={{ flex: 1, justifyContent: &apos;center&apos;, alignItems: &apos;center&apos; }}&gt;
      &lt;Button title=&quot;Pick an Audio File&quot; onPress={handlePickFile} /&gt;
      {fileInfo &amp;&amp; (
        &lt;Text style={{ marginTop: 20 }}&gt;
          File: {fileInfo.name} (Size: {fileInfo.size} bytes)
        &lt;/Text&gt;
      )}
    &lt;/View&gt;
  );
};

export default FilePickerExample;
</code></pre><p><strong>Install the library</strong>:If you haven&#x2019;t already, install the library by running:</p><pre><code class="language-bash">npm install react-native-document-picker
</code></pre><p>Or if you&apos;re using yarn:</p><pre><code class="language-bash">yarn add react-native-document-picker
</code></pre><h3 id="file-metadata-example">File Metadata Example:</h3><p>When a file is selected, the <code>DocumentPicker.pick()</code> method returns an object containing metadata about the file. Here is an example of the metadata you might get:</p><pre><code class="language-js">{
  uri: &apos;file:///storage/emulated/0/Download/song.mp3&apos;,
  type: &apos;audio/mp3&apos;,  // The MIME type of the file
  name: &apos;song.mp3&apos;,   // The file name
  size: 1234567,      // The size of the file in bytes
  fileCopyUri: &apos;content://com.android.providers.media.documents/document/...&apos;,
}
</code></pre><h3 id="file-types">File Types:</h3><p><code>react-native-document-picker</code> supports many file types, and you can filter what type of files users can pick. Here are some of the predefined types available:</p><p><strong>All Files</strong> (Generic for any file type):</p><pre><code class="language-js">DocumentPicker.types.allFiles
</code></pre><p><strong>Spreadsheets (Excel files)</strong>:</p><pre><code class="language-js">DocumentPicker.types.xls
</code></pre><p><strong>Plain Text</strong>:</p><pre><code class="language-js">DocumentPicker.types.plainText
</code></pre><p><strong>PDF Documents</strong>:</p><pre><code class="language-js">DocumentPicker.types.pdf
</code></pre><p><strong>Audio</strong>:</p><pre><code class="language-js">DocumentPicker.types.audio
</code></pre><p><strong>Images</strong>:</p><pre><code class="language-js">DocumentPicker.types.images
</code></pre><p>You can also combine multiple types. For example, to allow both images and audio:</p><pre><code class="language-js">type: [DocumentPicker.types.images, DocumentPicker.types.audio]
</code></pre><h3 id="key-methods">Key Methods:</h3><ul><li><strong><code>pick()</code></strong>: The main method used to open the file picker and select files. You can pass an object with options to specify file types and whether multiple files can be selected.</li><li><strong><code>pickMultiple()</code></strong>: Allows users to pick multiple files at once (iOS only).</li><li><strong><code>isCancel()</code></strong>: A utility method to check if the user canceled the file picking action.</li></ul><h3 id="platform-differences">Platform Differences:</h3><ul><li><strong>iOS</strong>: The library provides a native file picker. You can pick files from the local file system, including cloud services like iCloud Drive.</li><li><strong>Android</strong>: The library typically uses the device&apos;s file system picker, which may vary depending on the Android version and manufacturer. The file picker might not always show cloud storage options unless explicitly configured by the app.</li></ul><h3 id="use-cases">Use Cases:</h3><ol><li><strong>Allow users to upload documents, images, or audio files</strong> from their device to your app (e.g., for profile pictures, document submissions, etc.).</li><li><strong>Cloud storage integration</strong>: Some Android versions may allow cloud storage access via the file picker, but for full cloud file support, you&apos;ll typically need to integrate with a specific cloud API (e.g., Google Drive, iCloud).</li><li><strong>Handling PDFs or media files</strong>: If you need to open or process PDFs or media files (audio, video, etc.), this library helps by allowing users to select these files from their device.</li></ol><h3 id="summary">Summary:</h3><ul><li><strong>What it does</strong>: <code>react-native-document-picker</code> enables users to select files from their local storage (including documents, images, and audio files) and provides metadata about the selected file.</li><li><strong>Why it&apos;s useful</strong>: It simplifies the process of file selection in React Native apps, providing a consistent interface across iOS and Android.</li></ul><p>Let me know if you need more information or examples!</p>]]></content:encoded></item><item><title><![CDATA[Git Essential Commands]]></title><description><![CDATA[<h2 id="git-commands">Git Commands</h2>
<ul>
<li>which git</li>
<li>git --version</li>
<li>vi /etc/gitconfig</li>
<li>vi ~/.gitconfig : user level git config file</li>
<li>vi [PROJECT_PATH]/.git/config : project level git config file</li>
<li>git config --system : system level configuration</li>
<li>git config --global : user level configuration</li>
<li>git config : project level configuration</li>
<li>git config --global user.name &quot;[NAME]</li></ul>]]></description><link>https://blog.gyri.tech/git-essential-commands/</link><guid isPermaLink="false">682f2e72a37147040ff7651a</guid><dc:creator><![CDATA[Kaustubh Kesarkar]]></dc:creator><pubDate>Thu, 22 May 2025 14:04:57 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1556075798-4825dfaaf498?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGdpdHxlbnwwfHx8fDE3NDc5MjI2NzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<h2 id="git-commands">Git Commands</h2>
<ul>
<li>which git</li>
<li>git --version</li>
<li>vi /etc/gitconfig</li>
<li>vi ~/.gitconfig : user level git config file</li>
<li>vi [PROJECT_PATH]/.git/config : project level git config file</li>
<li>git config --system : system level configuration</li>
<li>git config --global : user level configuration</li>
<li>git config : project level configuration</li>
<li>git config --global user.name &quot;[NAME]&quot;</li>
<li>git config --list : list of config properties</li>
<li>git config --global core.excludefile ~/.gitignore_global : global config for gitignore</li>
<li>git config --global alias.[shortcut] [git-command] : shortcut for git command</li>
<li>git --help</li>
<li>git init : initialize repository</li>
<li>ls -la .git : shows git config files</li>
<li>git add . : add all uncommitted changes from project. dot (.) is current directory</li>
<li>git commit -m &quot;message&quot; : commit changes with message</li>
<li>git commit -am &quot;message&quot; : shorthand for <code>git add .</code> and <code>git commit -m &quot;message&quot;</code> (grabs everything. not tracked(new) and deleted files are not included)</li>
<li>git commit --amend -m &quot;message&quot; : this command is a convenient way to fix up the most recent commit.</li>
<li>git log : check git commit log</li>
<li>git log -2 : restrict show commit log to last two</li>
<li>git log --since=2016-01-12 : show commits after this date</li>
<li>git log --until=2016-01-12 : show commits till this date</li>
<li>git log --author=&quot;Kaustubh&quot; : show commits for specified author</li>
<li>git log --grep=&quot;st&quot; : show commits having specified text</li>
<li>git log --online : shows all logs in one line</li>
<li>git log --online -[number] : restrict log for given number</li>
<li>git log [SHA]..[SHA] : shows log between two mentioned commits</li>
<li>git log [file] : shows details about file</li>
<li>git log -p : more details about commit. Like code insertion etc</li>
<li>git log --stat --summary</li>
<li>git log --format=oneline : this is similar to `git log --oneline&quot;</li>
<li>git log --format=[oneline / short / medium / fuller / email / raw]</li>
<li>git log --graph</li>
<li>git log --oneline --graph --all --decorate</li>
<li>git status : difference between working directory, the staging index and teh repository</li>
<li>git status -s</li>
<li>git reset HEAD [file] : to unstage file</li>
<li>git reset HEAD</li>
<li>git reset --soft</li>
<li>git reset --mixed : default</li>
<li>git reset --hard</li>
<li>git reset --hard [SHA] : reverted to the specified SHA version</li>
<li>git diff : shows difference between the repository where HEAD is pointed at Vs working directory</li>
<li>git diff [file]</li>
<li>git diff --staged : shows difference between the staging index Vs working directory</li>
<li>git diff --cached : similar to <code>git diff --staged</code></li>
<li>git diff --color-words [file] : shows only different words</li>
<li>git diff [SHA] : difference between working directory and directory at that point of time</li>
<li>git diff [SHA] [file]</li>
<li>git diff [SHA]..[SHA] : difference between two commits</li>
<li>git diff [SHA]..[SHA] [file]</li>
<li>git diff -b : is same as <code>git diff --ignore-space-change</code></li>
<li>git diff -w : is same as <code>git diff --ignore-all-space</code></li>
<li>git diff [branch-1]..[branch-2]</li>
<li>git diff [branch-1]..[branch-2]^ : compare with previous commit of branch-2</li>
<li>git rm [file] : remove uncommitted file. similar to <code>git add</code>. use <code>git commit -m</code> to commit and remove file from repository.</li>
<li>git rm --cached [file] : remove from caching (or staging index)</li>
<li>git mv [orig_file] [newname_file] : rename file</li>
<li>git checkout -- [file] : to discard changes in working directory. &quot;--&quot; indicates from current branch only.</li>
<li>git checkout [SHA-key] -- [file] : revert to that SHA version</li>
<li>git revert [SHA] : revert and commit</li>
<li>git revert -n [SHA] : revert and not committed</li>
<li>git clean -n : test run of <code>git clean</code></li>
<li>git clean -f : final run of <code>git clean</code></li>
<li>git ls-tree HEAD</li>
<li>git ls-tree [branch-name]</li>
<li>git ls-tree [branch-name] [folder-path]</li>
<li>git ls-tree [branch-name]^ [folder-path] : earlier commit</li>
<li>git ls-tree [SHA]</li>
<li>git show [SHA]</li>
<li>git branch : list branch names in working directory</li>
<li>git branch [branch-name] : creates new branch</li>
<li>git branch -d [branch-name] : delete branch (before this make sure you are not working on this branch)</li>
<li>git branch -D [branch-name] : force delete branch</li>
<li>git branch -m [branch-new-name] : rename branch</li>
<li>git branch --merged :To see which branches are already merged into the branch you&#x2019;re on</li>
<li>git branch --no-merged : To see all the branches that contain work you haven&#x2019;t yet merged in</li>
<li>git branch -r : shows remote branches</li>
<li>git branch -a : shows local and remote branches</li>
<li>git branch [local-branch-name] [remote-branch-name] : creates new branch and tracking is set to the remote branch</li>
<li>git checkout [branch-name] : change working directory to specified branch name</li>
<li>git checkout -b [branch-name] : create and checkout at the same time for new branch</li>
<li>git checkout -b [local-branch-name] [remote-branch-name] : creates and checkout new branch and tracking is set to the remote branch</li>
<li>git merge [branch-name] : merge specified branch to current working branch</li>
<li>git merge --no-ff [branch-name] : no fast forward. create new commit</li>
<li>git merge -ff-only [branch-name] : do merge only for fast forward else abort</li>
<li>git merge --abort : abort the current merge</li>
<li>git stash save &quot;message&quot; : stash current changes</li>
<li>git stash list : shows list of items are in stash</li>
<li>git stash show stash@{NUMBER} : shows the details of that number</li>
<li>git stash show -p stash@{NUMBER} : shows the details of that number (more descriptive with -p option)</li>
<li>git stash pop : checkout changes are removes the stash</li>
<li>git stash apply : only checkout changes. keeps in the stash as well</li>
<li>git stash drop stash@{NUMBER} : delete the specified stash</li>
<li>git stash clear : clears the stash list</li>
<li>git remote : alias for remote branch</li>
<li>git remote add [alias] [remote-url] : add alias for remote url</li>
<li>git remote -v : more details about remote branch</li>
<li>git push -u [remote-alias] [branch-name] : push code changes to remote repository. -u option to track remote branch</li>
<li>git push : push current commits of the same branch to the origin (remote) branch</li>
<li>git push origin :[remote-branch-name] : deletes the branch from remote</li>
<li>git push origin --delete [remote-branch-name] : similar to <code>git push origin :[remote-branch-name]</code></li>
<li>git clone [git-remote-url] : creates with default folder</li>
<li>git clone [git-remote-url] [folder-name] : creates folder to clone remote repository</li>
<li>git fetch origin : sync (fetch) remote branch with local branch</li>
<li>git fetch --all : sync (fetch) all remote branches with local branches</li>
<li>git pull === git fetch + git merge</li>
</ul>
<h2 id="git-flow-commands">Git flow Commands</h2>
<ul>
<li>git flow init -d : initialise git flow</li>
<li>git flow feature start [feature-branch-name] : creates new feature branch</li>
<li>git flow feature publish [feature-branch-name] : publish feature branch to remote repository</li>
<li>git flow feature finish [feature-branch-name] : it deletes as well.</li>
</ul>
<h2 id="notes">Notes</h2>
<ul>
<li>SHA or SHA1 : Secure Hash Algorithm (1) <a href="https://en.wikipedia.org/wiki/SHA-1?ref=blog.gyri.tech">https://en.wikipedia.org/wiki/SHA-1</a></li>
<li>toggle between long commits : forward (f) or backward (b). space bar or enter to see more details</li>
<li>toggle fold long lines : minus sign (-) + shift + S + return. wrap it around instead of lines being truncated.</li>
<li>to return to long lines (unwrap) : minus sign (-) + S + return</li>
<li>cat .git/master : points to where HEAD (SHA) is located</li>
<li>cat .git/refs/heads/master : points to where HEAD (SHA) is located</li>
<li>project/.gitignore : file to specify which files or directory to ignore</li>
<li>.gitkeep : use this file to track empty folder</li>
<li>remote url details can be found at <code>.git/config</code> file</li>
</ul>
<h2 id="git-cherry-pick">Git cherry-pick</h2>
<ul>
<li>Cherry picking in git means to choose a commit from one branch and apply it onto another.</li>
<li>This is in contrast with other ways such as merge and rebase which normally apply many commits onto another branch.
<ol>
<li>Make sure you are on the branch you want to apply the commit to.<br>
<code>git checkout master</code></li>
<li>Execute the following:<br>
<code>git cherry-pick &lt;commit-hash&gt;</code></li>
</ol>
</li>
</ul>
<h2 id="git-windows-command">Git Windows command</h2>
<ul>
<li>git add --chmod=+x [FILE] : To mark file executable</li>
</ul>
<h2 id="contents">Contents</h2>
<img src="https://images.unsplash.com/photo-1556075798-4825dfaaf498?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGdpdHxlbnwwfHx8fDE3NDc5MjI2NzN8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="Git Essential Commands"><p><a href="https://www.linkedin.com/learning/git-essential-training-2023/get-started-with-git?ref=blog.gyri.tech">https://www.linkedin.com/learning/git-essential-training-2023/get-started-with-git</a></p>
<ul>
<li>
<p>Introduction</p>
<ul>
<li>Introduction</li>
<li>How to use the exercise files</li>
</ul>
</li>
<li>
<p>Welcome</p>
<ul>
<li>What you should know before watching this course</li>
<li>Using the exercise files</li>
</ul>
</li>
</ul>
<ol>
<li>
<p>What is Git?</p>
<ul>
<li>Understanding version control</li>
<li>The history of Git</li>
<li>About distributed version control</li>
<li>Who should use Git?</li>
</ul>
</li>
<li>
<p>Installing Git</p>
<ul>
<li>Installing Git on a Mac</li>
<li>Installing Git on Windows</li>
<li>Installing Git on Linux</li>
<li>Configuring Git</li>
<li>Exploring Git auto-completion</li>
<li>Using Git help</li>
</ul>
</li>
<li>
<p>Getting Started</p>
<ul>
<li>Initializing a repository</li>
<li>Understanding where Git files are stored</li>
<li>Performing your first commit</li>
<li>Writing commit messages</li>
<li>Viewing the commit log</li>
</ul>
</li>
<li>
<p>Git Concepts and Architecture</p>
<ul>
<li>Exploring the three-trees architecture</li>
<li>The Git workflow</li>
<li>Using hash values (SHA-1)</li>
<li>Working with the HEAD pointer</li>
</ul>
</li>
<li>
<p>Making Changes to Files</p>
<ul>
<li>Adding files</li>
<li>Editing files</li>
<li>Viewing changes with diff</li>
<li>Viewing only staged changes</li>
<li>Deleting files</li>
<li>Moving and renaming files</li>
</ul>
</li>
<li>
<p>Using Git with a Real Project</p>
<ul>
<li>Introducing the Explore California web site</li>
<li>Initializing Git</li>
<li>Editing the support phone number</li>
<li>Editing the backpack file name and links</li>
</ul>
</li>
<li>
<p>Undoing Changes</p>
<ul>
<li>Undoing working directory changes</li>
<li>Unstaging files</li>
<li>Amending commits</li>
<li>Retrieving old versions</li>
<li>Reverting a commit</li>
<li>Using reset to undo commits</li>
<li>Demonstrating a soft reset</li>
<li>Demonstrating a mixed reset</li>
<li>Demonstrating a hard reset</li>
<li>Removing untracked files</li>
</ul>
</li>
<li>
<p>Ignoring Files</p>
<ul>
<li>Using .gitignore files</li>
<li>Understanding what to ignore</li>
<li>Ignoring files globally</li>
<li>Ignoring tracked files</li>
<li>Tracking empty directories</li>
</ul>
</li>
<li>
<p>Navigating the Commit Tree</p>
<ul>
<li>Referencing commits</li>
<li>Exploring tree listings</li>
<li>Getting more from the commit log</li>
<li>Viewing commits</li>
<li>Comparing commits</li>
</ul>
</li>
<li>
<p>Branching</p>
<ul>
<li>Branching overview</li>
<li>Viewing and creating branches</li>
<li>Switching branches</li>
<li>Creating and switching branches</li>
<li>Switching branches with uncommitted changes</li>
<li>Comparing branches</li>
<li>Renaming branches</li>
<li>Deleting branches</li>
<li>Configuring the command prompt to show the branch</li>
</ul>
</li>
<li>
<p>Merging Branches</p>
<ul>
<li>Merging code</li>
<li>Using fast-forward merge vs. true merge</li>
<li>Merging conflicts</li>
<li>Resolving merge conflicts</li>
<li>Exploring strategies to reduce merge conflicts</li>
</ul>
</li>
<li>
<p>Stashing Changes</p>
<ul>
<li>Saving changes in the stash</li>
<li>Viewing stashed changes</li>
<li>Retrieving stashed changes</li>
<li>Deleting stashed changes</li>
</ul>
</li>
<li>
<p>Remotes</p>
<ul>
<li>Using local and remote repositories</li>
<li>Setting up a GitHub account</li>
<li>Adding a remote repository</li>
<li>Creating a remote branch</li>
<li>Cloning a remote repository</li>
<li>Tracking remote branches</li>
<li>Pushing changes to a remote repository</li>
<li>Fetching changes from a remote repository</li>
<li>Merging in fetched changes</li>
<li>Checking out remote branches</li>
<li>Pushing to an updated remote branch</li>
<li>Deleting a remote branch</li>
<li>Enabling collaboration</li>
<li>A collaboration workflow</li>
</ul>
</li>
<li>
<p>Tools and Next Steps</p>
<ul>
<li>Setting up aliases for common commands</li>
<li>Using SSH keys for remote login</li>
<li>Exploring integrated development environments</li>
<li>Exploring graphical user interfaces</li>
<li>Understanding Git hosting</li>
</ul>
</li>
<li>
<p>Conclusion</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[UNIX commands]]></title><description><![CDATA[<p></p><h2 id="basic-commands">Basic commands</h2>
<ul>
<li><code>su</code>: login as super user (e.g. root)</li>
<li><code>pwd</code>: current directory</li>
<li><code>ls</code>: list files</li>
<li><code>ls -l</code>: list files with detail info</li>
<li><code>bunzip2 [FILE_NAME]</code>: decompress file at same location</li>
<li><code>cp [FILE_1] [FILE_2]</code>: copy file1 to a file called file2</li>
<li><code>mv [FILE_1] [FILE_2]</code>: (move) rename</li></ul>]]></description><link>https://blog.gyri.tech/unix-commands/</link><guid isPermaLink="false">682031bdcb3275078952dad1</guid><dc:creator><![CDATA[Kaustubh Kesarkar]]></dc:creator><pubDate>Sun, 11 May 2025 05:13:24 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1629654297299-c8506221ca97?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGxpbnV4fGVufDB8fHx8MTc0Njk0MDMzNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1629654297299-c8506221ca97?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGxpbnV4fGVufDB8fHx8MTc0Njk0MDMzNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="UNIX commands"><p></p><h2 id="basic-commands">Basic commands</h2>
<ul>
<li><code>su</code>: login as super user (e.g. root)</li>
<li><code>pwd</code>: current directory</li>
<li><code>ls</code>: list files</li>
<li><code>ls -l</code>: list files with detail info</li>
<li><code>bunzip2 [FILE_NAME]</code>: decompress file at same location</li>
<li><code>cp [FILE_1] [FILE_2]</code>: copy file1 to a file called file2</li>
<li><code>mv [FILE_1] [FILE_2]</code>: (move) rename file1 to the name file2</li>
<li><code>mv [FILE_1] /[FOLDER_NAME]</code>: move file1 to the [FOLDER_NAME] directory</li>
<li><code>rm *</code>: deletes everything in a subdirectory (<a href="http://cmgm.stanford.edu/classes/unix/rm.html?ref=blog.gyri.tech">http://cmgm.stanford.edu/classes/unix/rm.html</a>)</li>
<li><code>rm -r *</code>: deletes everything in directory</li>
<li><code>rm test.txt</code>: removes only file with a name &quot;test.txt&quot; on the end</li>
<li><code>tail -500 FILE_NAME | less</code>: see last 500 lines of files</li>
<li><code>cat [FILE_NAME]</code>: view file</li>
</ul>
<h2 id="size">Size</h2>
<ul>
<li><code>df -k</code>: disk size</li>
<li><code>df -g</code>: disk size</li>
<li><code>df -h</code>: disk size</li>
<li><code>du -s [FOLDER_NAME]</code>: folder size info</li>
<li><code>du -sh ./</code>: current directory size</li>
<li><code>du -sh -- *(D) | sort -k1n</code>: order parent folder by size in ascending order</li>
<li><code>ls -l | grep ^- | sort -nr -k 5</code>: order files as per size</li>
</ul>
<h2 id="basic-network-commands">Basic network commands</h2>
<ul>
<li><code>ifconfig</code>: similar to ipconfig (display network interface parameters)</li>
<li><code>hostname</code>: get machine&apos;s name</li>
<li><code>/etc/init.d/network restart</code>: restart network</li>
</ul>
<h2 id="install">Install</h2>
<ul>
<li><code>rpm -ivh</code>: install rpm bin file</li>
</ul>
<h2 id="init-file">Init file</h2>
<ul>
<li><code>/etc/init.d/chkconfig &lt;SERVICE_NAME&gt; off</code>: off service at startup with super user (<a href="http://www.aboutlinux.info/2006/04/enabling-and-disabling-services-during_01.html?ref=blog.gyri.tech">http://www.aboutlinux.info/2006/04/enabling-and-disabling-services-during_01.html</a>)</li>
<li><code>/etc/init.d/chkconfig &lt;SERVICE_NAME&gt; on</code>: start service at startup with super user (<a href="http://www.aboutlinux.info/2006/04/enabling-and-disabling-services-during_01.html?ref=blog.gyri.tech">http://www.aboutlinux.info/2006/04/enabling-and-disabling-services-during_01.html</a>)</li>
</ul>
<h2 id="terminal">Terminal</h2>
<ul>
<li><code>tty</code>: to check terminal</li>
<li><code>ps -ef | grep java</code>: check the java processes</li>
<li><code>kill -9 &lt;process_id&gt;</code>: &lt;process_id&gt; is returned by prev command.</li>
</ul>
<h2 id="user">User</h2>
<ul>
<li><code>startx</code>: start GUI</li>
<li><code>useradd &lt;user&gt;</code>: create new user</li>
<li><code>passwd &lt;user&gt;</code>: change password for user</li>
<li><code>groupadd &lt;groupname&gt;</code>: create new group</li>
<li><code>usermod -g &lt;groupname&gt; &lt;user&gt;</code>: Assign a user to a primary group</li>
<li><code>usermod -G &lt;groupname&gt; &lt;user&gt;</code>: Assign a user to secondary groups</li>
<li><code>whoami</code>: current user</li>
<li><code>sudo loginctl enable-linger</code>: Keep a user service alive after logout</li>
</ul>
<h2 id="vi-editor">vi Editor</h2>
<ul>
<li><code>vi [FILE_NAME]</code>: Open or edit a file.</li>
<li><code>Esc</code>: Switch to Command mode.</li>
<li><code>oo / o</code>: insert new line after current line</li>
<li><code>OO / O</code>: insert new line before curren line</li>
<li><code>i</code>: Switch to Insert mode.</li>
<li><code>[ESC] i ENTER</code>: insert into file to location</li>
<li><code>:w</code>: Save and continue editing</li>
<li><code>:wq</code> or <code>ZZ</code>: Save and quit/exit vi</li>
<li><code>:x</code>: to file save</li>
<li><code>:q!</code>: Quit vi and do not save changes</li>
<li><code>yy</code>: Yank (copy) a line of text</li>
<li><code>p</code>: Paste a line of yanked text below the current line</li>
<li><code>o</code>: Open a new line under the current line</li>
<li><code>O</code>: Open a new line above the current line</li>
<li><code>A</code>: Append to the end of the line</li>
<li><code>a</code>: Append after the cursor&#x2019;s current position</li>
<li><code>I</code>: Insert text at the beginning of the current line</li>
<li><code>b</code>: Go to the beginning of the word</li>
<li><code>e</code>: Go to the end of the word</li>
<li><code>x</code>: Delete a single character</li>
<li><code>dd</code>: Delete an entire line</li>
<li><code>[X]dd</code>: Delete X number of lines</li>
<li><code>[X]yy</code>: Yank X number of lines</li>
<li><code>G</code>: Go to the last line in a file</li>
<li><code>[X]G</code>: Go to line X in a file</li>
<li><code>gg</code>: Go to the first line in a file</li>
<li><code>:num</code>: Display the current line&#x2019;s line number</li>
<li><code>h</code>: Move left one character</li>
<li><code>l</code>: Move right one character</li>
<li><code>j</code>: Move down one line</li>
<li><code>k</code>: Move up one line</li>
</ul>
<h2 id="set-env">Set ENV</h2>
<ul>
<li><code>vi ~/.bash_profile</code>: env variable setup file</li>
<li><code>source ~/.bash_profile</code>: export env variables to system</li>
<li><code>export JAVA_HOME=/root/java/jdk1.7.0_45</code></li>
<li><code>export MAVEN_HOME=/root/java/apache-maven-3.1.1</code></li>
<li><code>export ANT_HOME=/root/java/apache-ant-1.9.2</code></li>
<li><code>export PATH=$PATH:${JAVA_HOME}/bin:${MAVEN_HOME}/bin:${ANT_HOME}/bin</code></li>
</ul>
<h2 id="increase-swap-size">Increase swap size</h2>
<ul>
<li><code>dd if=/dev/zero of=/.swapfile bs=1M count=2048</code></li>
<li><code>mkswap -v1 /.swapfile</code></li>
<li><code>swapon /.swapfile</code></li>
</ul>
<h2 id="miscellaneous">Miscellaneous</h2>
<ul>
<li>ubuntu jdk setup: <a href="http://www.mkyong.com/java/how-to-install-java-jdk-on-ubuntu-linux/?ref=blog.gyri.tech">http://www.mkyong.com/java/how-to-install-java-jdk-on-ubuntu-linux/</a></li>
<li>clear temp directory: <a href="http://forums.opensuse.org/english/get-technical-help-here/how-faq-forums/unreviewed-how-faq/412640-clear-temp-files-boot.html?ref=blog.gyri.tech">http://forums.opensuse.org/english/get-technical-help-here/how-faq-forums/unreviewed-how-faq/412640-clear-temp-files-boot.html</a></li>
<li><code>sync; echo 3 &gt; /proc/sys/vm/drop_caches</code>: clear buffer cache</li>
<li><code>/tmp/VMwareDnD</code>: VM ware DragNDrop files - verify and delete</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Migrate Data from MongoDB to PostgreSQL]]></title><description><![CDATA[<blockquote><strong>ETL </strong>(Extract, Transform, Load)</blockquote><p>ETL (Extract, Transform, Load) is a fundamental process in databases and data warehousing used to move and process data from one system to another. It consists of three main steps:</p><ol><li><strong>Extract</strong> &#x2013; Retrieve data from multiple sources (e.g., databases, APIs, files).</li><li><strong>Transform</strong> &#x2013; Clean, filter,</li></ol>]]></description><link>https://blog.gyri.tech/migrate-data-from-mongodb-to-postgresql/</link><guid isPermaLink="false">67d2b2eecf0c31040a1a7e2b</guid><dc:creator><![CDATA[Shubham Kiran Lokhande]]></dc:creator><pubDate>Mon, 17 Mar 2025 07:59:29 GMT</pubDate><media:content url="https://blog.gyri.tech/content/images/2025/03/Migration.png" medium="image"/><content:encoded><![CDATA[<blockquote><strong>ETL </strong>(Extract, Transform, Load)</blockquote><img src="https://blog.gyri.tech/content/images/2025/03/Migration.png" alt="Migrate Data from MongoDB to PostgreSQL"><p>ETL (Extract, Transform, Load) is a fundamental process in databases and data warehousing used to move and process data from one system to another. It consists of three main steps:</p><ol><li><strong>Extract</strong> &#x2013; Retrieve data from multiple sources (e.g., databases, APIs, files).</li><li><strong>Transform</strong> &#x2013; Clean, filter, and modify the data to fit the target system&#x2019;s structure and requirements.</li><li><strong>Load</strong> &#x2013; Store the transformed data into the target database or data warehouse.</li></ol><p>ETL is widely used for <strong>data migration, integration, and analytics</strong>. For example, moving data from <strong>MongoDB to PostgreSQL</strong> is ETL process that ensures structured storage and efficient querying.</p><blockquote><strong>Change Data Capture(CDC) </strong></blockquote><p>The ETL processor supports real-time <strong>Change Data Capture (CDC)</strong> to keep data synchronized between MongoDB and PostgreSQL With CDC:</p><ul><li><strong>Watchers monitor MongoDB collections</strong> for real-time changes.</li><li><strong>Changes (inserts, updates, replacements, and deletes) are automatically captured</strong> and replicated to PostgreSQL.</li><li><strong>Each ETL job can have its own CDC configuration</strong>, ensuring flexibility.</li><li><strong>CDC complements batch ETL</strong>, providing both real-time and scheduled data synchronization.</li></ul><blockquote><strong><em>Methods for Implementing CDC</em></strong></blockquote><h3 id="1-apache-nifi"><strong>1) Apache NiFi</strong></h3><p><a href="https://nifi.apache.org/?ref=blog.gyri.tech" rel="noopener">Apache NiFi</a> is a <strong>powerful data flow automation tool</strong> designed for ETL processes and real-time data streaming. Originally developed by the NSA and later open-sourced by the Apache Software Foundation, NiFi offers:</p><ul><li><strong>An intuitive drag-and-drop interface</strong> for building complex data pipelines.</li><li><strong>Support for multiple data sources and destinations</strong>, making integration seamless.</li><li><strong>Real-time data ingestion and transformation</strong>, ideal for high-volume workloads.</li></ul><h3 id="2-custom-jobs"><strong>2) Custom Jobs</strong></h3><p><a href="https://qms-etl2.gyri.tech/?ref=blog.gyri.tech" rel="noopener">Custom ETL Jobs</a> automate data transfer between different databases, ensuring seamless integration. These jobs:</p><ul><li><strong>Migrate data from MongoDB to PostgreSQL on a scheduled basis</strong> (e.g., daily).</li><li><strong>Define source and target databases</strong> to structure data efficiently.</li><li><strong>Process data incrementally</strong> based on a specific field (e.g., <code>creationDate</code>).</li><li><strong>Handle data in batches</strong> (e.g., 500 records at a time for optimal performance).</li><li><strong>Leverage CDC</strong> to track and update changes, ensuring data consistency with minimal manual intervention.</li></ul><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.gyri.tech/content/images/2025/03/Migration2-2.png" class="kg-image" alt="Migrate Data from MongoDB to PostgreSQL" loading="lazy" width="546" height="631"><figcaption><span style="white-space: pre-wrap;">Migrate Data</span></figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Firebase Push Notifications Setup in React Native (Android specific)]]></title><description><![CDATA[<p>Firebase Cloud Messaging (FCM) is a service that allows you to send notifications and messages to users across platforms like iOS, Android, and the web.This guide will walk you through how to set up Firebase Push Notifications in a React Native app for Android.</p><h3 id="prerequisites"><strong>Prerequisites</strong></h3><ul><li>A React Native project</li></ul>]]></description><link>https://blog.gyri.tech/how-to-integrate-firebase-push-notifications-in-react-native/</link><guid isPermaLink="false">67a06513cf0c31040a1a7d3b</guid><dc:creator><![CDATA[Ankita Shelake]]></dc:creator><pubDate>Thu, 06 Feb 2025 05:51:00 GMT</pubDate><media:content url="https://blog.gyri.tech/content/images/2025/02/1_W_GKuVzLWjsKmuwdJ0qWgQ.png" medium="image"/><content:encoded><![CDATA[<img src="https://blog.gyri.tech/content/images/2025/02/1_W_GKuVzLWjsKmuwdJ0qWgQ.png" alt="Firebase Push Notifications Setup in React Native (Android specific)"><p>Firebase Cloud Messaging (FCM) is a service that allows you to send notifications and messages to users across platforms like iOS, Android, and the web.This guide will walk you through how to set up Firebase Push Notifications in a React Native app for Android.</p><h3 id="prerequisites"><strong>Prerequisites</strong></h3><ul><li>A React Native project setup (if you don&apos;t have one, you can create it by running <code>npx react-native init MyProject</code>).</li><li>Node.js installed on your machine.</li><li>Firebase account and Firebase project setup in the Firebase Console.</li></ul><h3 id="step-1-create-a-firebase-project">Step 1: Create a Firebase Project:<br></h3><figure class="kg-card kg-image-card"><img src="https://blog.gyri.tech/content/images/2025/02/image-10.png" class="kg-image" alt="Firebase Push Notifications Setup in React Native (Android specific)" loading="lazy" width="2000" height="1300" srcset="https://blog.gyri.tech/content/images/size/w600/2025/02/image-10.png 600w, https://blog.gyri.tech/content/images/size/w1000/2025/02/image-10.png 1000w, https://blog.gyri.tech/content/images/size/w1600/2025/02/image-10.png 1600w, https://blog.gyri.tech/content/images/2025/02/image-10.png 2000w" sizes="(min-width: 720px) 720px"></figure><ol><li>Go to <a href="https://firebase.google.com/?gad_source=1&amp;gclid=CjwKCAiAtYy9BhBcEiwANWQQL_3G1WupNuemiDQxtWOTSSAuArwvVW8VW3DbHY0SBz7Kp5ULSrxvfhoC1cwQAvD_BwE&amp;gclsrc=aw.ds&amp;ref=blog.gyri.tech" rel="noreferrer">Firebase Console</a>.</li><li>Click on <strong>Add Project</strong> and follow the steps to create a new project.</li><li><strong>Go to your Firebase Console</strong> and select your project.</li><li>In the <strong>left-hand sidebar</strong>, navigate to <strong>Project settings</strong> (gear icon at the top).</li><li>In the <strong>Project Settings</strong>, under the <strong>General</strong> tab, you&apos;ll see the <strong>Your apps</strong> section.</li><li>There should be an option to <strong>Download google-services.json</strong> right next to your Android app listing. Just click that, and it&#x2019;ll download the file for you.</li><li>Once downloaded, <strong>move</strong> the <code>google-services.json</code> file into the <strong>app folder</strong> of your Android project (usually located at <code>app/</code> in your project structure).</li></ol><pre><code class="language-MyProject/">MyProject/
  &#x251C;&#x2500;&#x2500; android/
  &#x2502;    &#x251C;&#x2500;&#x2500; app/
  &#x2502;    &#x2502;    &#x251C;&#x2500;&#x2500; google-services.json  &lt;-- place it here
  &#x2502;    &#x2502;    &#x251C;&#x2500;&#x2500; src/
  &#x2502;    &#x2502;    &#x2514;&#x2500;&#x2500; ...
  &#x251C;&#x2500;&#x2500; ios/
  &#x251C;&#x2500;&#x2500; lib/
  &#x2514;&#x2500;&#x2500; ...
</code></pre><h3 id="step-2-install-required-dependencies"><strong>Step 2: Install Required Dependencies:</strong></h3><p>To use Firebase Cloud Messaging in your React Native app, install these packages via ``npm``<br><code>npm install @react-native-firebase/app @react-native-firebase/messaging @notifee/react-native</code></p><ul><li><strong>@react-native-firebase/app</strong>: This is the core library that connects your React Native app with Firebase.</li><li><strong>@react-native-firebase/messaging</strong>: This library allows you to interact with Firebase Cloud Messaging (FCM) for push notifications.</li><li><strong>@notifee/react-native</strong>: Notifee enables developers to rapidly build rich notifications with a simple API interface, whilst taking care of complex problems such as scheduling, background tasks, device API compatibility &amp; more.</li></ul><h3 id="step-3-set-up-and-configure-each-library-according-to-official-documentation">Step 3: Set Up and Configure Each Library According to Official Documentation:</h3><p>a. Setup <code>@react-native-firebase/app</code> (Firebase Core SDK):<br>`npm install @react-native-firebase/app`</p><ol><li>Modify <code>android/build.gradle</code></li></ol><pre><code class="language-groovy">buildscript {
  dependencies {
    // ... other dependencies
    // NOTE: if you are on react-native 0.71 or below, you must not update
    //       the google-services plugin past version 4.3.15 as it requires gradle &gt;= 7.3.0
    classpath &apos;com.google.gms:google-services:4.4.2&apos;
    // Add me --- /\
  }
}</code></pre><ol start="3"><li>Modify <code>android/app/build.gradle</code></li></ol><pre><code class="language-groovy">apply plugin: &apos;com.android.application&apos;
apply plugin: &apos;com.google.gms.google-services&apos; // &lt;- Add this line</code></pre><ol start="4"><li>rebuilding</li></ol><pre><code class="language-bash"># Android apps
npx react-native run-android</code></pre><blockquote><a href="https://rnfirebase.io/?ref=blog.gyri.tech" rel="noreferrer">Document file to learn more about this library.</a></blockquote><p>b. Setup <code>@react-native-firebase/messaging</code>:</p><blockquote><a href="https://rnfirebase.io/?ref=blog.gyri.tech" rel="noreferrer">You can refer to the official documentation for setting up messaging.</a></blockquote><p>c. Setup <code>@notifee/react-native</code>:</p><blockquote><a href="https://notifee.app/react-native/docs/overview?ref=blog.gyri.tech" rel="noreferrer">Document file to learn more about this library.</a></blockquote><h3 id="step-4-get-fcm-device-token">Step 4: Get FCM Device Token:</h3><p><strong>Import the Firebase Messaging Library</strong>: You need to import <code>@react-native-firebase/messaging</code> to retrieve the FCM token.</p><pre><code>import messaging from &apos;@react-native-firebase/messaging&apos;;

async function getDeviceToken() {
  try {
    // Get the FCM token
    const token = await messaging().getToken();
    console.log(&apos;FCM Device Token:&apos;, token);
    return token;
  } catch (error) {
    console.error(&apos;Error fetching FCM token:&apos;, error);
  }
}

// Call the function to retrieve the token
getDeviceToken();
</code></pre><h3 id="step-5-save-the-fcm-token-to-firestore-or-your-backend">Step 5: <strong>Save the FCM Token to Firestore or Your Backend:</strong></h3><p>To send the FCM token to your <strong>backend server</strong>, follow these steps:</p><ol><li><strong>Set up the backend server</strong> (using Node.js or any other backend of your choice).</li><li><strong>Send the token from the React Native app</strong> to your backend, which will save the token in your database.</li></ol><h3 id="step-6-send-a-test-push-notification-from-firebase-console">Step 6: Send a Test Push Notification from Firebase Console:</h3><ol><li><strong>Go to Firebase Console</strong>:</li></ol><ul><li>Navigate to the <a rel="noopener">Firebase Console</a>.</li><li>Select your project.</li></ul><ol start="2"><li><strong>Open the Cloud Messaging Section</strong>:<br></li></ol><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.gyri.tech/content/images/2025/02/image-3.png" class="kg-image" alt="Firebase Push Notifications Setup in React Native (Android specific)" loading="lazy" width="1877" height="573" srcset="https://blog.gyri.tech/content/images/size/w600/2025/02/image-3.png 600w, https://blog.gyri.tech/content/images/size/w1000/2025/02/image-3.png 1000w, https://blog.gyri.tech/content/images/size/w1600/2025/02/image-3.png 1600w, https://blog.gyri.tech/content/images/2025/02/image-3.png 1877w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Click on Create your first campaign</span></figcaption></figure><figure class="kg-card kg-image-card"><img src="https://blog.gyri.tech/content/images/2025/02/image-4.png" class="kg-image" alt="Firebase Push Notifications Setup in React Native (Android specific)" loading="lazy" width="1103" height="983" srcset="https://blog.gyri.tech/content/images/size/w600/2025/02/image-4.png 600w, https://blog.gyri.tech/content/images/size/w1000/2025/02/image-4.png 1000w, https://blog.gyri.tech/content/images/2025/02/image-4.png 1103w" sizes="(min-width: 720px) 720px"></figure><ol start="3"><li><strong>Compose the Message</strong>:</li></ol><figure class="kg-card kg-image-card"><img src="https://blog.gyri.tech/content/images/2025/02/image-6.png" class="kg-image" alt="Firebase Push Notifications Setup in React Native (Android specific)" loading="lazy" width="1099" height="1160" srcset="https://blog.gyri.tech/content/images/size/w600/2025/02/image-6.png 600w, https://blog.gyri.tech/content/images/size/w1000/2025/02/image-6.png 1000w, https://blog.gyri.tech/content/images/2025/02/image-6.png 1099w" sizes="(min-width: 720px) 720px"></figure><ol start="4"><li><strong>Target the Notification</strong>:</li></ol><ul><li>Under <strong>Target</strong>, select <strong>Single Device</strong>.</li><li><strong>Enter the FCM Token</strong>: In the &quot;Token&quot; field, paste the device token that you retrieved in the previous step.<ul><li>If you&#x2019;ve saved the token in Firestore, retrieve it from there, or use the token logged in the console.<br></li></ul></li></ul><ol start="5"><li><strong>Send the Notification</strong>:</li></ol><ul><li>After filling in the details, click on <strong>Send Test Message</strong>.</li><li>You should see a confirmation message stating that the notification was sent successfully.<br></li></ul><figure class="kg-card kg-image-card"><img src="https://blog.gyri.tech/content/images/2025/02/image-7.png" class="kg-image" alt="Firebase Push Notifications Setup in React Native (Android specific)" loading="lazy" width="1019" height="1117" srcset="https://blog.gyri.tech/content/images/size/w600/2025/02/image-7.png 600w, https://blog.gyri.tech/content/images/size/w1000/2025/02/image-7.png 1000w, https://blog.gyri.tech/content/images/2025/02/image-7.png 1019w" sizes="(min-width: 720px) 720px"></figure><blockquote><em>Conclusion</em></blockquote><blockquote><em>Setting up Firebase Push Notifications in a React Native app for Android is a straightforward process if you follow the steps carefully. By integrating the required libraries, configuring Firebase, and obtaining the FCM device token, you can enable seamless push notifications for your users.</em></blockquote>]]></content:encoded></item></channel></rss>