<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: VISHNU</title>
    <description>The latest articles on DEV Community by VISHNU (@vishnu_venkat).</description>
    <link>https://dev.to/vishnu_venkat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4212297%2Fe4139ab7-58d4-491d-981a-1c586265e99c.jpg</url>
      <title>DEV Community: VISHNU</title>
      <link>https://dev.to/vishnu_venkat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vishnu_venkat"/>
    <language>en</language>
    <item>
      <title>I Built a Real App With Zero Servers and Zero Cost</title>
      <dc:creator>VISHNU</dc:creator>
      <pubDate>Thu, 29 Oct 2026 13:24:04 +0000</pubDate>
      <link>https://dev.to/vishnu_venkat/i-built-a-real-app-with-zero-servers-and-zero-cost-27m1</link>
      <guid>https://dev.to/vishnu_venkat/i-built-a-real-app-with-zero-servers-and-zero-cost-27m1</guid>
      <description>&lt;p&gt;No server. No database. No bill. Just React, Google Apps Script and a GoogleSheet.&lt;/p&gt;

&lt;p&gt;by Vishnu&lt;/p&gt;




&lt;h2&gt;
  
  
  The question that kills side projects
&lt;/h2&gt;

&lt;p&gt;I have started many side projects. Most of them died at the same moment.&lt;/p&gt;

&lt;p&gt;Not at a hard feature. Not at a bug. They died when I asked myself one&lt;br&gt;
question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Which database should I use?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question looks small. It is not. It turns into hosting. Then connection&lt;br&gt;
strings. Then migrations. Then a free plan that goes to sleep every 15&lt;br&gt;
minutes. Then a monthly bill — to store 400 rows that only I will ever read.&lt;/p&gt;

&lt;p&gt;So in my last project, I did something different. &lt;strong&gt;I did not answer the&lt;br&gt;
question.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My app keeps its data in a Google Sheet. Google Apps Script works as the API.&lt;br&gt;
React is the front end. There is no server for me to run. There is no&lt;br&gt;
database for me to back up. There is no bill.&lt;/p&gt;

&lt;p&gt;It works. I spent three weekends learning Docker once, so this is a little&lt;br&gt;
bit annoying.&lt;/p&gt;

&lt;p&gt;This article is about &lt;strong&gt;the pattern&lt;/strong&gt;, not my app. At the end I will show you&lt;br&gt;
what I built, and six other things you could build the same way.&lt;/p&gt;


&lt;h2&gt;
  
  
  What you replace
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You normally need&lt;/th&gt;
&lt;th&gt;You use this instead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A database server&lt;/td&gt;
&lt;td&gt;A Google Sheet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrations&lt;/td&gt;
&lt;td&gt;Adding a column. By clicking.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An API server&lt;/td&gt;
&lt;td&gt;Apps Script web app (&lt;code&gt;doGet&lt;/code&gt; / &lt;code&gt;doPost&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An admin panel&lt;/td&gt;
&lt;td&gt;...the sheet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backups&lt;/td&gt;
&lt;td&gt;Google Drive history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An "export my data" button&lt;/td&gt;
&lt;td&gt;It is already a sheet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at the last row. In a normal app, "let the user see their own data" is&lt;br&gt;
work. Someone has to build it.&lt;/p&gt;

&lt;p&gt;Here, you get it free. The data was always a sheet. You only built a nicer&lt;br&gt;
window for it.&lt;/p&gt;


&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Three parts. One of them is a spreadsheet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────┐
│  BROWSER  —  React                       │
│                                          │
│    Components                            │
│         │                                │
│    Hooks  (fetch · cache · state)        │
│         │                                │
│    api.js                                │
└──────────────────┬───────────────────────┘
                   │
                   │   GET   ?action=listTopics
                   │   POST  { action, ...data }
                   ▼
┌──────────────────────────────────────────┐
│  GOOGLE APPS SCRIPT  —  the "backend"    │
│                                          │
│    doGet  /  doPost                      │
│    routes on ?action=                    │
└──────────────────┬───────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────┐
│  GOOGLE SHEET  —  the "database"         │
│                                          │
│    Tab: Topics   ← the index             │
│    id · name · sheetName                 │
│         │                                │
│         ├──→  Tab: Java                  │
│         └──→  Tab: JavaScript            │
└──────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How the data sits in the sheet
&lt;/h3&gt;

&lt;p&gt;One tab works as the index. It lists all your groups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;topicId | topicName | sheetName | createdAt | deletedAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every group then gets its own tab. One record is one row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;questionId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;questionText&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;answerText&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;difficulty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updatedAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two small choices here save a lot of pain later:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Use an id, not a name.&lt;/strong&gt; &lt;code&gt;topicId&lt;/code&gt; is a UUID. Every API call uses the&lt;br&gt;
id. Never the name. So when you rename something, nothing breaks. The name is&lt;br&gt;
just a label. The id is the real thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Do not really delete.&lt;/strong&gt; Deleting fills in &lt;code&gt;deletedAt&lt;/code&gt; and leaves the row&lt;br&gt;
alone. It is one extra column. It has already saved me once.&lt;/p&gt;


&lt;h2&gt;
  
  
  Build it in 5 steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make a Google Sheet.&lt;/strong&gt; This is your database now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click Extensions → Apps Script.&lt;/strong&gt; Write &lt;code&gt;doGet&lt;/code&gt; and &lt;code&gt;doPost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click Deploy → New deployment → Web app.&lt;/strong&gt; Copy the &lt;code&gt;/exec&lt;/code&gt; URL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Point your React app at that URL.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;There is no step 5. That is the whole point.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your Apps Script just checks what the app asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;listTopics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;listTopics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;listQuestions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;listQuestions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;addQuestion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;addQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renameTopic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;renameTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I needed ten of these. That was enough for the whole app.&lt;/p&gt;




&lt;h2&gt;
  
  
  3 problems you will hit
&lt;/h2&gt;

&lt;p&gt;I hit all three. You do not have to.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Your POST breaks if you add a header
&lt;/h3&gt;

&lt;p&gt;This one took me a full afternoon.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;Content-Type: application/json&lt;/code&gt; to your POST. This is the normal thing.&lt;br&gt;
Every tutorial does it. And every save will fail.&lt;/p&gt;

&lt;p&gt;What makes it worse: your GET calls still work fine. So you look in the wrong&lt;br&gt;
place for hours.&lt;/p&gt;

&lt;p&gt;Here is why. That header makes the request "not simple". So the browser first&lt;br&gt;
sends a small check request called &lt;code&gt;OPTIONS&lt;/code&gt;. Apps Script cannot answer an&lt;br&gt;
&lt;code&gt;OPTIONS&lt;/code&gt; request. It just ignores it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: send no headers at all.&lt;/strong&gt; The body goes as plain text, and your&lt;br&gt;
script reads it itself.&lt;/p&gt;

&lt;p&gt;It looks wrong. So I left a note in my code, for future me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * POST deliberately sends no headers.
 *
 * Adding Content-Type makes this a non-simple CORS request; the browser then
 * sends a preflight OPTIONS, which Google Apps Script cannot answer, and every
 * write fails. Without headers the body goes as text/plain and the script
 * parses it itself (JSON.parse of e.postData.contents). Do not "fix" this.
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That note is the most useful thing in my repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Sheet names ignore CAPITAL letters
&lt;/h3&gt;

&lt;p&gt;I renamed a tab from &lt;code&gt;JS&lt;/code&gt; to &lt;code&gt;js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My app made a second tab called &lt;strong&gt;&lt;code&gt;js (2)&lt;/code&gt;&lt;/strong&gt;. All my questions looked like&lt;br&gt;
they were gone.&lt;/p&gt;

&lt;p&gt;Here is what happened. My code asked "does a tab with this name already&lt;br&gt;
exist?" using &lt;code&gt;getSheetByName()&lt;/code&gt;. That function does not care about capital&lt;br&gt;
letters. So &lt;code&gt;JS&lt;/code&gt; and &lt;code&gt;js&lt;/code&gt; are the same name to it.&lt;/p&gt;

&lt;p&gt;The tab compared itself to itself. Then it decided it was a copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; compare ids, not names. Ids do not care about capital letters.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Your dates are not dates
&lt;/h3&gt;

&lt;p&gt;Write a date into a cell as a string. Sheets saves it as &lt;strong&gt;text&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It looks like a date on screen. But it sorts like a word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; write real &lt;code&gt;Date&lt;/code&gt; objects. If you already saved strings, you now&lt;br&gt;
need a script to fix them. So now you do have a migration. Sorry.&lt;/p&gt;


&lt;h2&gt;
  
  
  It is slow. Build for that.
&lt;/h2&gt;

&lt;p&gt;Every Apps Script call takes &lt;strong&gt;1 to 3 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is not broken. But it is slow enough that your app feels tired.&lt;/p&gt;

&lt;p&gt;Two things fixed most of it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Remember what you already loaded
&lt;/h3&gt;

&lt;p&gt;If a user goes back to something they already opened, do not ask again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// show what we already have, then quietly refresh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before: 1 to 3 seconds. After: about &lt;strong&gt;1 millisecond&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not ask for what you already know
&lt;/h3&gt;

&lt;p&gt;My sidebar shows how many questions each topic has.&lt;/p&gt;

&lt;p&gt;My first version loaded every topic's questions just to count them. Even the&lt;br&gt;
topic already open on screen — the one whose questions were already loaded.&lt;/p&gt;

&lt;p&gt;With 20 topics, opening the app made &lt;strong&gt;22 requests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now the app passes in what it already knows, and only asks about the rest.&lt;br&gt;
Small fix. Very satisfying.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I built: QMaster
&lt;/h2&gt;

&lt;p&gt;QMaster is where I keep interview questions.&lt;/p&gt;

&lt;p&gt;You paste a list of questions. Any format. It reads them, finds duplicates,&lt;br&gt;
and shows you the result. Nothing is saved until you say yes.&lt;/p&gt;

&lt;p&gt;The hard part was pasted text. People's lists look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. What is the JVM and how does it
   differ from the JRE?
2. Explain inheritance.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line 2 belongs to question 1. A computer does not know that.&lt;/p&gt;

&lt;p&gt;So I made a simple rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the text has markers (&lt;code&gt;1.&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;Q:&lt;/code&gt;), then &lt;strong&gt;a marker starts a new
question&lt;/strong&gt;, and any line without a marker joins the question above it.&lt;/li&gt;
&lt;li&gt;If there are no markers at all, &lt;strong&gt;every line is its own question&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It covers both normal cases. And if it gets something wrong, you can fix it&lt;br&gt;
on screen before saving.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvr5z7646ljz96kqv3lct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvr5z7646ljz96kqv3lct.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;the main screen.&lt;/strong&gt; Show a topic open, questions listed,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;sidebar and filter tabs visible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvsgic9dpplqgpzlz8p60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvsgic9dpplqgpzlz8p60.png" alt=" " width="255" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;on a phone.&lt;/strong&gt; One normal view, one with the menu open.&lt;/p&gt;

&lt;p&gt;But QMaster is not the point. &lt;strong&gt;The pattern is the point.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6 more apps you could build this way
&lt;/h2&gt;

&lt;p&gt;These are &lt;strong&gt;ideas. I have not built them.&lt;/strong&gt; I have added the sheet columns&lt;br&gt;
for each one, so you can see how little work it really is.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Your sheet columns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Task manager&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Add tasks, set priority, mark done, filter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id · title · priority · status · dueDate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Expense tracker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Add income and costs, see your balance&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id · date · amount · category · type · note&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Portfolio builder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enter your skills and projects, make a page&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id · section · title · description · link&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kanban board&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;To Do, Doing, Done. Drag cards between them&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id · title · column · order&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quiz app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Questions, options, score at the end&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id · question · optionA–D · correct&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Notes app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Notes with tags and Markdown preview&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id · title · body · tags · updatedAt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one uses the same three parts: an index tab, one tab per group, and&lt;br&gt;
&lt;code&gt;doGet&lt;/code&gt; / &lt;code&gt;doPost&lt;/code&gt; with an action name.&lt;/p&gt;

&lt;p&gt;Only the kanban board needs a little thought, because of the &lt;code&gt;order&lt;/code&gt; column.&lt;br&gt;
The answer is simple: use a number, and renumber the cards after a drop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One real advantage over &lt;code&gt;localStorage&lt;/code&gt;:&lt;/strong&gt; your data is not stuck in one&lt;br&gt;
browser on one laptop. It is in Google Drive. It syncs, because Google does&lt;br&gt;
the syncing for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Now the honest part
&lt;/h2&gt;

&lt;p&gt;This pattern is easy. But it costs you something. Here it is.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;What it costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No servers&lt;/td&gt;
&lt;td&gt;Every call takes 1 to 3 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No migrations&lt;/td&gt;
&lt;td&gt;No real search — you filter in the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data you can open and read&lt;/td&gt;
&lt;td&gt;Not safe for many users at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No bill&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No real security&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last one needs plain words.&lt;/p&gt;

&lt;p&gt;An Apps Script web app set to "anyone with the link" has &lt;strong&gt;no login and no&lt;br&gt;
password.&lt;/strong&gt; Anyone who gets that URL can read and change your sheet.&lt;/p&gt;

&lt;p&gt;And here is a trap. Putting the URL in a &lt;code&gt;.env&lt;/code&gt; file feels like a fix. It is&lt;br&gt;
not. Vite puts &lt;code&gt;VITE_&lt;/code&gt; values &lt;strong&gt;inside your JavaScript&lt;/strong&gt; when it builds. So&lt;br&gt;
the URL is still there, in the code the browser downloads.&lt;/p&gt;

&lt;p&gt;It does keep the URL out of GitHub. That matters, because bots scan GitHub&lt;br&gt;
for things like this. But anyone can still find it in DevTools on a live&lt;br&gt;
site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So:&lt;/strong&gt; fine for your own personal app. Not fine for real users. If you want&lt;br&gt;
it truly hidden, you need a small serverless function in front of it.&lt;/p&gt;

&lt;p&gt;Two more honest things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My project has no tests yet.&lt;/strong&gt; I checked it by hand, with the linter and
the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no live demo link.&lt;/strong&gt; The app points at my own sheet, and I would
rather not hand out the key.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Should you do this?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes, if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is a personal app&lt;/li&gt;
&lt;li&gt;A few hundred or a few thousand rows&lt;/li&gt;
&lt;li&gt;One person writing the data&lt;/li&gt;
&lt;li&gt;You want to actually finish it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No, if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Many people write at the same time&lt;/li&gt;
&lt;li&gt;You need real login&lt;/li&gt;
&lt;li&gt;You need real search&lt;/li&gt;
&lt;li&gt;Speed matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the thing I really learned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure is a choice.&lt;/strong&gt; We reach for a database because tutorials&lt;br&gt;
taught us to. But those tutorials solve problems most side projects do not&lt;br&gt;
have.&lt;/p&gt;

&lt;p&gt;For 400 rows, a sheet is better. Not because it is easier to code. Because&lt;br&gt;
when something goes wrong, &lt;strong&gt;you can open it and look.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Try it on something small. The worst case is you lose a weekend. The best&lt;br&gt;
case is you stop asking the question that keeps killing your projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/VishnuVenkat07/Q-Master-" rel="noopener noreferrer"&gt;https://github.com/VishnuVenkat07/Q-Master-&lt;/a&gt;  &lt;strong&gt;(it private you want means contact email - &lt;a href="mailto:vishnuvishnuv08@gmail.com"&gt;vishnuvishnuv08@gmail.com&lt;/a&gt; )&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; not published — see "the honest part"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Written by **Vishnu&lt;/em&gt;&lt;em&gt;. If you build something with this, I would like to see&lt;br&gt;
it. Tell me in the comments.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>react</category>
      <category>product</category>
      <category>ai</category>
      <category>career</category>
    </item>
  </channel>
</rss>
