Skip to content

Commit fcdcd11

Browse files
authored
Merge pull request #308 from Devsol-01/main
feat: Document Security Architecture
2 parents 57bb76f + 6b99613 commit fcdcd11

4 files changed

Lines changed: 545 additions & 0 deletions

File tree

docs/CIRCLE_LIFECYCLE.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Circle Lifecycle: Creation, Rounds, Payouts, and Dissolution
2+
3+
## Circle Statuses
4+
5+
| Status | Description |
6+
|--------|-------------|
7+
| `PENDING` | Circle created but not yet started |
8+
| `ACTIVE` | Circle is running; contributions and payouts are in progress |
9+
| `COMPLETED` | All rounds have finished |
10+
| `CANCELLED` | Circle was cancelled before completion |
11+
12+
## Full Lifecycle
13+
14+
### Stage 1 — Creation
15+
**Who**: Any authenticated user (becomes the organizer)
16+
17+
**How**: `POST /api/circles` with:
18+
- `name`, `description`
19+
- `contributionAmount` — how much each member contributes per round
20+
- `contributionFrequencyDays` — how many days between rounds
21+
- `maxRounds` — total number of rounds (= total number of members who will receive a payout)
22+
23+
**What happens**:
24+
- Circle is created with status: **ACTIVE** and `currentRound`: 1
25+
- Organizer is automatically added as the first `CircleMember` with `rotationOrder`: 1
26+
- If deploying the smart contract: `initialize_circle()` is called on-chain with the same parameters
27+
28+
### Stage 2 — Member Recruitment
29+
**Who**: Organizer
30+
31+
**How**: Share the circle ID or invite link. Members join via `POST /api/circles/:id/join`.
32+
33+
**On-chain**: Organizer calls `add_member(organizer, newMemberAddress)` for each new member.
34+
35+
**Constraints**:
36+
- Maximum 50 members by default (hard cap 100 on-chain)
37+
- Members cannot join once the circle is full
38+
39+
**Rotation order**: Before the first round begins, the organizer should call `shuffle_rotation()` on the smart contract to randomise the payout order using a Fisher-Yates shuffle seeded by the ledger sequence number.
40+
41+
### Stage 3 — Contribution Round
42+
**Who**: All members
43+
44+
**How**: Each member calls `POST /api/circles/:id/contribute` with `{ amount }`.
45+
46+
**What happens per contribution**:
47+
- A `Contribution` record is created with status: **COMPLETED** and the current round number
48+
- The member's `totalContributed` is incremented in `CircleMember`
49+
- All members who have **NOT** yet contributed this round receive a reminder email
50+
- If all members have now contributed, the member whose `rotationOrder` matches `currentRound` receives a payout alert email
51+
52+
**On-chain**: The member calls `contribute(member, amount)` which transfers tokens from the member's wallet to the contract. The contract advances the round deadline when all members have contributed.
53+
54+
### Stage 4 — Payout
55+
**Who**: The member whose turn it is (determined by `rotationOrder === currentRound`)
56+
57+
**How**: Member calls `claim_payout(member)` on the smart contract.
58+
59+
**Payout amount**: `contributionAmount × memberCount`
60+
61+
**Constraints**:
62+
- If `shuffle_rotation()` was called, the contract enforces that only the correct member can claim
63+
- Each member can only receive one payout (`hasReceivedPayout` flag)
64+
65+
**After payout**: `currentRound` increments and the next round begins.
66+
67+
### Stage 5 — Repeat
68+
Stages 3 and 4 repeat for each round until `currentRound > maxRounds`.
69+
70+
### Stage 6 — Completion
71+
When all `maxRounds` have been completed:
72+
- Circle status is updated to **COMPLETED**
73+
- All members have received exactly one payout
74+
- Total paid out = `contributionAmount × memberCount × maxRounds`
75+
76+
## Partial Withdrawals
77+
78+
Members can withdraw a portion of their contributed funds at any time during an active circle, subject to a **10% penalty**.
79+
80+
**How**: Call `partial_withdraw(member, amount)` on the smart contract.
81+
82+
**Available balance**: `totalContributed - totalWithdrawn`
83+
84+
**Net received**: `amount - (amount × 10 / 100)`
85+
86+
**Example**: Requesting a 100 XLM withdrawal returns 90 XLM; 10 XLM is kept as a penalty.
87+
88+
**Note**: Partial withdrawals are blocked during **Panicked** state — use `emergency_refund` instead.
89+
90+
## Emergency Scenarios
91+
92+
### Organizer Panic
93+
If the organizer detects a critical issue, they can call `panic(organizer)` on the smart contract. This:
94+
- Immediately sets circle status to **Panicked**
95+
- Blocks all contributions, payouts, and partial withdrawals
96+
- Enables `emergency_refund` for every member (**no penalty**)
97+
98+
### Member-Initiated Dissolution
99+
Any member can start a dissolution vote via `start_dissolution_vote`. If the vote passes:
100+
- Circle status transitions to **Dissolved**
101+
- Each member calls `dissolve_and_refund` to receive `totalContributed - totalWithdrawn` (**no penalty**)
102+
103+
## Member Rotation Order
104+
105+
The payout rotation determines who receives the pooled funds each round.
106+
107+
**Default**: Members are added in join order with sequential `rotationOrder` values.
108+
109+
**Shuffled**: The organizer calls `shuffle_rotation()` before round 1 to randomise the order. The shuffle uses Fisher-Yates with a seed derived from the ledger sequence number, making it verifiably random and tamper-resistant.
110+
111+
**Example** with 4 members and 100 XLM contribution:
112+
113+
| Round | Payout Recipient | Amount Received |
114+
|-------|------------------|-----------------|
115+
| 1 | Member at rotationOrder 1 | 400 XLM |
116+
| 2 | Member at rotationOrder 2 | 400 XLM |
117+
| 3 | Member at rotationOrder 3 | 400 XLM |
118+
| 4 | Member at rotationOrder 4 | 400 XLM |
119+
120+
Each member contributes 100 XLM × 4 rounds = 400 XLM total, and receives 400 XLM once. **Net result**: zero loss, but each member gets access to a lump sum when it's their turn.
121+
122+
## Circle Statistics (Dashboard)
123+
124+
The dashboard displays these computed values for each circle:
125+
126+
| Stat | Calculation |
127+
|------|-------------|
128+
| **Members** | `circle.members.length` |
129+
| **Current Round** | `circle.currentRound / circle.maxRounds` |
130+
| **Per Contribution** | `circle.contributionAmount XLM` |
131+
| **Payout Amount** | `circle.contributionAmount × circle.members.length XLM` |
132+

docs/ENVIRONMENT_VARIABLES.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Environment Variables: Purpose, Values, and Security Rules
2+
3+
## Variable Categories
4+
5+
### Public Variables (browser-accessible)
6+
Prefixed with `NEXT_PUBLIC_`. These are embedded into the JavaScript bundle at build time and are visible to anyone who inspects the page source. **Never put secrets here.**
7+
8+
### Server-Only Variables
9+
No prefix. Only available in API routes and server-side code. Never exposed to the browser.
10+
11+
## Complete Variable Reference
12+
13+
### Stellar Network
14+
15+
| Variable | Required | Public | Description |
16+
|----------|----------|--------|-------------|
17+
| `NEXT_PUBLIC_STELLAR_NETWORK` | Yes | Yes | `testnet` or `mainnet` |
18+
| `NEXT_PUBLIC_STELLAR_HORIZON_URL` | Yes | Yes | Stellar Horizon REST API endpoint |
19+
| `NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE` | Yes | Yes | Network passphrase used when signing transactions |
20+
| `NEXT_PUBLIC_SOROBAN_RPC_URL` | Yes | Yes | Soroban RPC endpoint for smart contract calls |
21+
| `NEXT_PUBLIC_AJO_CONTRACT_ADDRESS` | Yes* | Yes | Deployed Soroban contract ID. Can be empty until contract is deployed |
22+
| `NEXT_PUBLIC_WALLET_NETWORK_DETAILS_PUBLIC_KEY` | No | Yes | Wallet network details key, defaults to public |
23+
24+
### Database
25+
26+
| Variable | Required | Public | Description |
27+
|----------|----------|--------|-------------|
28+
| `DATABASE_URL` | Yes | No | Prisma database connection string |
29+
30+
### Authentication
31+
32+
| Variable | Required | Public | Description |
33+
|----------|----------|--------|-------------|
34+
| `JWT_SECRET` | Yes | No | Secret key used to sign and verify JWT access tokens. Must be a long random string in production |
35+
36+
### API
37+
38+
| Variable | Required | Public | Description |
39+
|----------|----------|--------|-------------|
40+
| `NEXT_PUBLIC_API_URL` | Yes | Yes | Base URL for API calls from the browser |
41+
42+
### Email
43+
44+
| Variable | Required | Public | Description |
45+
|----------|----------|--------|-------------|
46+
| `RESEND_API_KEY` | Yes* | No | Resend API key for sending transactional emails. Required for email notifications to work |
47+
48+
## Values by Environment
49+
50+
### Local Development
51+
52+
```bash
53+
# Stellar — Testnet
54+
NEXT_PUBLIC_STELLAR_NETWORK=testnet
55+
NEXT_PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
56+
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
57+
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
58+
NEXT_PUBLIC_AJO_CONTRACT_ADDRESS=
59+
60+
# Database — SQLite
61+
DATABASE_URL="file:./dev.db"
62+
63+
# Auth — any non-empty string is fine locally
64+
JWT_SECRET=dev-secret-key-change-in-production
65+
66+
# API
67+
NEXT_PUBLIC_API_URL=http://localhost:3000/api
68+
69+
# Email — optional for local dev
70+
RESEND_API_KEY=
71+
```
72+
73+
### Staging / Testnet
74+
75+
```bash
76+
NEXT_PUBLIC_STELLAR_NETWORK=testnet
77+
NEXT_PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
78+
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
79+
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
80+
NEXT_PUBLIC_AJO_CONTRACT_ADDRESS=<testnet-contract-id>
81+
82+
DATABASE_URL=postgresql://user:password@host:5432/ajo_staging
83+
84+
JWT_SECRET=<generate-with-openssl-rand-hex-32>
85+
86+
NEXT_PUBLIC_API_URL=https://staging.yourdomain.com/api
87+
88+
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
89+
```
90+
91+
### Production / Mainnet
92+
93+
```bash
94+
NEXT_PUBLIC_STELLAR_NETWORK=mainnet
95+
NEXT_PUBLIC_STELLAR_HORIZON_URL=https://horizon.stellar.org
96+
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"
97+
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban.stellar.org
98+
NEXT_PUBLIC_AJO_CONTRACT_ADDRESS=<mainnet-contract-id>
99+
100+
DATABASE_URL=postgresql://user:password@host:5432/ajo_production
101+
102+
JWT_SECRET=<generate-with-openssl-rand-hex-32>
103+
104+
NEXT_PUBLIC_API_URL=https://yourdomain.com/api
105+
106+
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
107+
```
108+
109+
## Generating a Secure JWT Secret
110+
111+
```bash
112+
# Using Node.js
113+
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
114+
115+
# Using OpenSSL
116+
openssl rand -hex 32
117+
```
118+
119+
The output is a 64-character hex string. Use this as `JWT_SECRET` in production.
120+
121+
## Database URL Formats
122+
123+
### SQLite (development only):
124+
```
125+
DATABASE_URL="file:./dev.db"
126+
```
127+
128+
### PostgreSQL (staging and production):
129+
```
130+
DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE
131+
```
132+
133+
### With SSL (required by most managed PostgreSQL providers):
134+
```
135+
DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE?sslmode=require
136+
```
137+
138+
## Setting Variables in Vercel
139+
140+
1. Go to your Vercel project → **Settings****Environment Variables**
141+
2. Add each variable with the appropriate value
142+
3. Select which environments it applies to: **Production**, **Preview**, **Development**
143+
4. **Redeploy** for changes to take effect
144+
145+
**Note**: Variables prefixed with `NEXT_PUBLIC_` must be set before the build runs — they are baked into the bundle.
146+
147+
## Security Rules
148+
149+
- Never commit `.env.local` or any file containing real secrets to Git
150+
- `.env.local` is already in `.gitignore`
151+
- Only `.env.example` (with placeholder values) should be committed
152+
- Rotate `JWT_SECRET` if it is ever exposed — all existing sessions will be invalidated
153+
- **Never** put `JWT_SECRET`, `DATABASE_URL`, or `RESEND_API_KEY` in any `NEXT_PUBLIC_` variable
154+

0 commit comments

Comments
 (0)